diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1e34eb6cc61..a8bd477484e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -65,18 +65,14 @@ make lint make lint-fix ``` +Note: Linting is **NOT** run on linux/arm64 or linux/arm platforms to avoid issues. + **Check code locks:** ```bash make check-locks ``` **Note:** Not available on linux/arm64 or linux/arm. -**Check JavaScript/TypeScript formatting (in app directory):** -```bash -make check-prettier -``` - -**Important:** Linting is **NOT** run on linux/arm64 or linux/arm platforms to avoid issues. ### Building Kopia CLI @@ -120,7 +116,7 @@ make kopia-ui make test ``` **Time:** ~2-4 minutes -**Runs:** All unit tests with gotestsum, excludes TestIndexBlobManagerStress +**Runs:** All unit tests with gotestsum **Timeout:** 1200s (20 minutes) per test **Format:** pkgname-and-test-fails @@ -132,12 +128,6 @@ make test-with-coverage **Time:** ~3-5 minutes **Note:** Used by Code Coverage workflow. Sets KOPIA_COVERAGE_TEST=1 -### Index Blob Tests (Separate) -```bash -make test-index-blob-v0 -``` -**Runs:** TestIndexBlobManagerStress (excluded from standard tests due to duration) - ### Integration Tests ```bash make build-integration-test-binary # Build test binary first @@ -146,6 +136,11 @@ make integration-tests **Time:** ~5-10 minutes **Requires:** KOPIA_INTEGRATION_EXE environment variable +**Race Detector Tests:** +```bash +make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s +``` + ### CI Test Suites ```bash make ci-tests # Runs: vet + test @@ -157,7 +152,7 @@ make ci-integration-tests # Runs: robustness-tool-tests + socket-activation-tes make provider-tests PROVIDER_TEST_TARGET=... ``` **Time:** 15 minutes timeout -**Requires:** KOPIA_PROVIDER_TEST=true, credentials for storage backend, rclone binary +**Requires:** KOPIA_PROVIDER_TEST=true, credentials for storage backend. **Note:** Tests various cloud storage providers (S3, Azure, GCS, etc.) ### Other Test Types @@ -167,11 +162,6 @@ make provider-tests PROVIDER_TEST_TARGET=... - `make stress-test` - Stress tests (1 hour timeout) - `make htmlui-e2e-test` - HTML UI end-to-end tests (10 minutes timeout) -**Race Detector Tests:** -```bash -make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s -``` - ## Common Issues & Workarounds ### Build Issues @@ -197,7 +187,7 @@ make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s 3. **Integration test binary:** Must build integration test binary explicitly with `make build-integration-test-binary` before running integration tests. -4. **Provider tests require environment:** Provider tests need KOPIA_PROVIDER_TEST=true and rclone binary available. +4. **Provider tests require environment:** Provider tests need KOPIA_PROVIDER_TEST=true and storage credentials. ### Environment Variables @@ -205,9 +195,8 @@ make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s - `UNIX_SHELL_ON_WINDOWS=true` - Required for Windows builds - `KOPIA_COVERAGE_TEST=1` - Enable coverage testing - `KOPIA_INTEGRATION_EXE` - Path to integration test binary -- `TESTING_ACTION_EXE` - Path to testing action binary - `KOPIA_PROVIDER_TEST=true` - Enable provider tests -- `RCLONE_EXE` - Path to rclone binary for provider tests + ## Project Structure @@ -270,9 +259,8 @@ make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s ### Configuration Files - `.golangci.yml` - Linter config with 40+ enabled linters, custom rules -- `.codecov.yml` - Code coverage reporting config -- `.goreleaser.yml` - Release automation config - `.github/workflows/*.yml` - GitHub Actions workflows (19 workflow files) +- `.codecov.yml` - Code coverage reporting config ## GitHub Actions Workflows @@ -408,4 +396,7 @@ make test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s - node - JavaScript runtime for app builds - hugo - Static site generator for website -10. **Trust these instructions** - These instructions have been validated by running all commands. Only search for additional information if something fails or if these instructions are incomplete or incorrect. +10. Do not commit executables or binary artifacts to the git repository. + Do not modify `.gitignore` files. + +11. **Trust these instructions** - These instructions have been validated by running all commands. Only search for additional information if something fails or if these instructions are incomplete or incorrect. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7f6a19dd925..3078efb44ad 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,8 @@ updates: open-pull-requests-limit: 5 schedule: interval: weekly + cooldown: + default-days: 4 ignore: # htmluibuild is explicitly updated by us - dependency-name: "github.com/kopia/htmluibuild" @@ -29,6 +31,8 @@ updates: open-pull-requests-limit: 5 schedule: interval: monthly + cooldown: + default-days: 4 groups: github-actions: patterns: @@ -41,6 +45,8 @@ updates: directory: "/app" schedule: interval: monthly + cooldown: + default-days: 7 groups: # create once-per-week PR for all KopiaUI dependency bumps, that usually includes # electron, electron-builder, etc. diff --git a/.github/instructions/go.copilot-instructions.md b/.github/instructions/go.copilot-instructions.md index c945717c916..9deaa92650a 100644 --- a/.github/instructions/go.copilot-instructions.md +++ b/.github/instructions/go.copilot-instructions.md @@ -78,10 +78,12 @@ Refer to the linter configuration in `.golangci.yml` for style checks and standa ### Formatting +- Indent with tabs - Use `gofumt` to format code - Use `goimports` to manage ordering of `import` statements - Keep line length reasonable (no hard limit, but consider readability) -- Add blank lines to separate logical groups of code, adhering to the linter constraints. +- Add blank lines to separate logical groups of code, adhering to the linter constraints +- Ensure the file ends with a trailing newline `\n` ### Comments @@ -330,6 +332,8 @@ Refer to the linter configuration in `.golangci.yml` for style checks and standa - `go mod`: Manage dependencies - `go generate`: Code generation +`make lint vet` runs `go vet` and `golangci-lint` + ### Development Practices - Run tests before committing diff --git a/.github/workflows/auto-merge.yml.disabled b/.github/workflows/auto-merge.yml.disabled index 25e747244cd..46a204a719b 100644 --- a/.github/workflows/auto-merge.yml.disabled +++ b/.github/workflows/auto-merge.yml.disabled @@ -7,7 +7,7 @@ jobs: auto-merge: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ahmadnassri/action-dependabot-auto-merge@45fc124d949b19b6b8bf6645b6c9d55f4f9ac61a #v2.6.6 with: # auto-merge rules are in /.github/auto-merge.yml diff --git a/.github/workflows/code-coverage.yml.disabled b/.github/workflows/code-coverage.yml.disabled index 0b0e2a383c2..3ba1314df80 100644 --- a/.github/workflows/code-coverage.yml.disabled +++ b/.github/workflows/code-coverage.yml.disabled @@ -12,21 +12,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Run Tests run: make test-with-coverage - name: Upload Coverage - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: coverage.txt - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs path: .logs/**/*.log diff --git a/.github/workflows/compat-test.yml.disabled b/.github/workflows/compat-test.yml.disabled index c3081f9937b..54b777fe8fa 100644 --- a/.github/workflows/compat-test.yml.disabled +++ b/.github/workflows/compat-test.yml.disabled @@ -14,17 +14,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Compat Test run: make compat-tests - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs path: .logs/**/*.log diff --git a/.github/workflows/dependency-review.yml.disabled b/.github/workflows/dependency-review.yml.disabled index 8ac26bdd90b..714b0588ee3 100644 --- a/.github/workflows/dependency-review.yml.disabled +++ b/.github/workflows/dependency-review.yml.disabled @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 #v4.8.2 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 #v4.9.0 diff --git a/.github/workflows/endurance-test.yml.disabled b/.github/workflows/endurance-test.yml.disabled index 2f62239c940..79daccecb64 100644 --- a/.github/workflows/endurance-test.yml.disabled +++ b/.github/workflows/endurance-test.yml.disabled @@ -26,11 +26,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' check-latest: true @@ -38,7 +38,7 @@ jobs: - name: Endurance Tests run: make endurance-tests - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs path: .logs/**/*.log diff --git a/.github/workflows/htmlui-tests.yml.disabled b/.github/workflows/htmlui-tests.yml.disabled index 88960a6d3c1..4ed5fb8db33 100644 --- a/.github/workflows/htmlui-tests.yml.disabled +++ b/.github/workflows/htmlui-tests.yml.disabled @@ -27,19 +27,21 @@ jobs: runs-on: macos-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' check-latest: true id: go + - name: Install gotestsum + run: make install-gotestsum - name: Run Tests run: make htmlui-e2e-test - name: Upload Screenshots - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: .screenshots/**/*.png if-no-files-found: ignore diff --git a/.github/workflows/license-check.yml.disabled b/.github/workflows/license-check.yml.disabled index 282a4e5228b..711a00deb0d 100644 --- a/.github/workflows/license-check.yml.disabled +++ b/.github/workflows/license-check.yml.disabled @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Download dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a74bc260de3..13d4d7769e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,8 @@ env: ENABLE_UNICODE_FILENAMES: ${{ secrets.ENABLE_UNICODE_FILENAMES }} # set (to any value other than false) to trigger very long filenames testing ENABLE_LONG_FILENAMES: ${{ secrets.ENABLE_LONG_FILENAMES }} +permissions: + contents: read jobs: build: strategy: @@ -24,13 +26,15 @@ jobs: os: [ubuntu-latest] name: Lint runs-on: ${{ matrix.os }} + permissions: + security-events: write steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Lint diff --git a/.github/workflows/make.yml.disabled b/.github/workflows/make.yml.disabled index 4e1bc0f8ac5..a3a669363eb 100644 --- a/.github/workflows/make.yml.disabled +++ b/.github/workflows/make.yml.disabled @@ -36,11 +36,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' id: go @@ -62,7 +62,7 @@ jobs: CSC_KEYCHAIN: ${{ secrets.CSC_KEYCHAIN }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} - if: ${{ contains(matrix.os, 'macos') }} + if: ${{ contains(matrix.os, 'macos') && github.event_name != 'pull_request' }} - name: Install Windows signing tools # install signing tools and credentials for macOS and Windows outside of main # build process. @@ -90,9 +90,9 @@ jobs: WINDOWS_SIGN_TOOL: ${{ secrets.WINDOWS_SIGN_TOOL }} # macOS signing certificate (base64-encoded), used by Electron Builder - MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + MACOS_SIGNING_IDENTITY: ${{ github.event_name != 'pull_request' && secrets.MACOS_SIGNING_IDENTITY || '' }} - name: Upload Kopia Artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: kopia-${{ matrix.os }} path: | @@ -113,7 +113,7 @@ jobs: dist/kopia-ui/*.yml if-no-files-found: ignore - name: Upload Kopia Binary - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: kopia_binaries-${{ matrix.os }} path: | @@ -128,21 +128,21 @@ jobs: needs: build if: github.event_name != 'pull_request' && github.repository == 'kopia/kopia' steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Install Linux-specific packages run: "sudo apt-get install -y createrepo-c" - name: Download Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: kopia-* merge-multiple: true path: dist - name: Download Kopia Binaries - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: kopia_binaries-* merge-multiple: true @@ -185,7 +185,7 @@ jobs: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - name: Bump Homebrew formula - uses: dawidd6/action-homebrew-bump-formula@3428a0601bba3173ec0bdcc945be23fa27aa4c31 # v5 + uses: dawidd6/action-homebrew-bump-formula@1446dca236b0440c6f02723a3f14f13be2c04ab0 # v7 # only bump formula for tags which don't contain '-' # this excludes vx.y.z-rc1 if: github.ref_type == 'tag' && !contains(github.ref_name, '-') diff --git a/.github/workflows/ossf-scorecard.yml.disabled b/.github/workflows/ossf-scorecard.yml.disabled index afc4258f3f9..3eb0ae1845d 100644 --- a/.github/workflows/ossf-scorecard.yml.disabled +++ b/.github/workflows/ossf-scorecard.yml.disabled @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout repo" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - @@ -39,13 +39,13 @@ jobs: - # Upload the results to GitHub's code scanning dashboard. name: "Upload to results to dashboard" - uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.5 with: sarif_file: results.sarif category: ossf - name: "Upload analysis results as 'Job Artifact'" - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/providers-core.yml.disabled b/.github/workflows/providers-core.yml.disabled index 557c5cdaccd..7ce690d84c5 100644 --- a/.github/workflows/providers-core.yml.disabled +++ b/.github/workflows/providers-core.yml.disabled @@ -24,12 +24,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: ${{ github.event.inputs.ref_name || github.ref }} - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Install Dependencies diff --git a/.github/workflows/providers-extra.yml.disabled b/.github/workflows/providers-extra.yml.disabled index 31268041c09..aeeff06c9da 100644 --- a/.github/workflows/providers-extra.yml.disabled +++ b/.github/workflows/providers-extra.yml.disabled @@ -24,12 +24,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: ${{ github.event.inputs.ref_name || github.ref }} - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Install Dependencies diff --git a/.github/workflows/race-detector.yml.disabled b/.github/workflows/race-detector.yml.disabled index ab297fe6e09..a7e24e22911 100644 --- a/.github/workflows/race-detector.yml.disabled +++ b/.github/workflows/race-detector.yml.disabled @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Unit Tests diff --git a/.github/workflows/stale.yml.disabled b/.github/workflows/stale.yml.disabled index e5b3210e3b6..c162224d2e5 100644 --- a/.github/workflows/stale.yml.disabled +++ b/.github/workflows/stale.yml.disabled @@ -14,7 +14,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: # process older PRs first ascending: true diff --git a/.github/workflows/stress-test.yml.disabled b/.github/workflows/stress-test.yml.disabled index 3305401fcc2..ab1bf7a661a 100644 --- a/.github/workflows/stress-test.yml.disabled +++ b/.github/workflows/stress-test.yml.disabled @@ -18,17 +18,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Stress Test run: make stress-test - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs path: .logs/**/*.log diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 189d6197143..8d14a14fd5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,11 +30,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Setup @@ -44,7 +44,7 @@ jobs: - name: Tests run: make ci-tests - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs-${{ matrix.os }} path: .logs/**/*.log diff --git a/.github/workflows/volume-shadow-copy-test.yml.disabled b/.github/workflows/volume-shadow-copy-test.yml.disabled index f4de9626c00..e369b9a31f1 100644 --- a/.github/workflows/volume-shadow-copy-test.yml.disabled +++ b/.github/workflows/volume-shadow-copy-test.yml.disabled @@ -15,11 +15,11 @@ jobs: runs-on: windows-latest steps: - name: Check out repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' - name: Install gsudo @@ -32,7 +32,7 @@ jobs: - name: Non-Admin Test run: gsudo -i Medium make os-snapshot-tests - name: Upload Logs - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: logs path: .logs/**/*.log diff --git a/Makefile b/Makefile index 54b1a0fbb03..69a1e86dc17 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ endif endif GOTESTSUM_FORMAT=pkgname-and-test-fails -GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=skipped +GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --hide-summary=output,skipped GO_TEST?=$(gotestsum) $(GOTESTSUM_FLAGS) -- LINTER_DEADLINE=1200s @@ -87,13 +87,19 @@ endif lint-and-log: $(linter) $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) | tee .linterr.txt -lint-all: $(linter) +lint-windows: $(linter) GOOS=windows GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) + +lint-darwin: $(linter) + GOOS=darwin GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) + GOOS=darwin GOARCH=arm64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) + +lint-linux: $(linter) GOOS=linux GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) GOOS=linux GOARCH=arm64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) GOOS=linux GOARCH=arm $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) - GOOS=darwin GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) - GOOS=darwin GOARCH=arm64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) + +lint-all: $(linter) lint-windows lint-darwin lint-linux GOOS=openbsd GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) GOOS=freebsd GOARCH=amd64 $(linter) --timeout $(LINTER_DEADLINE) run $(linter_flags) @@ -279,13 +285,13 @@ test-with-coverage: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE) test-with-coverage: $(gotestsum) $(TESTING_ACTION_EXE) $(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -tags testing -count=$(REPEAT_TEST) -short -covermode=atomic -coverprofile=coverage.txt --coverpkg $(COVERAGE_PACKAGES) -timeout $(UNIT_TESTS_TIMEOUT) ./... -test: GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=skipped --jsonfile=.tmp.unit-tests.json +test: GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=output --jsonfile=.tmp.unit-tests.json test: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE) test: $(gotestsum) $(TESTING_ACTION_EXE) $(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -tags testing -count=$(REPEAT_TEST) -timeout $(UNIT_TESTS_TIMEOUT) -skip '^TestIndexBlobManagerStress$$' ./... -$(gotestsum) tool slowest --jsonfile .tmp.unit-tests.json --threshold 1000ms -test-index-blob-v0: GOTESTSUM_FLAGS=--format=pkgname --no-summary=output,skipped +test-index-blob-v0: GOTESTSUM_FLAGS=--format=pkgname --no-summary=output test-index-blob-v0: $(gotestsum) $(TESTING_ACTION_EXE) $(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -tags testing -count=$(REPEAT_TEST) -timeout $(UNIT_TESTS_TIMEOUT) -run '^TestIndexBlobManagerStress$$' ./repo/content/indexblob/... @@ -295,15 +301,17 @@ PROVIDER_TEST_TARGET=... provider-tests: export KOPIA_PROVIDER_TEST=true provider-tests: export RCLONE_EXE=$(rclone) -provider-tests: GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=skipped --jsonfile=.tmp.provider-tests.json +provider-tests: GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=output --jsonfile=.tmp.provider-tests.json provider-tests: $(gotestsum) $(rclone) $(MINIO_MC_PATH) $(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -count=$(REPEAT_TEST) -timeout 15m ./repo/blob/$(PROVIDER_TEST_TARGET) -$(gotestsum) tool slowest --jsonfile .tmp.provider-tests.json --threshold 1000ms ALLOWED_LICENSES=Apache-2.0;MIT;BSD-2-Clause;BSD-3-Clause;CC0-1.0;ISC;MPL-2.0;CC-BY-3.0;CC-BY-4.0;ODC-By-1.0;WTFPL;0BSD;Python-2.0;BSD;Unlicense -license-check: $(wwhrd) app-node-modules +license-check-go: $(wwhrd) $(wwhrd) check + +license-check: license-check-go app-node-modules (cd app && npx license-checker --summary --production --onlyAllow "$(ALLOWED_LICENSES)") vtest: $(gotestsum) @@ -318,7 +326,7 @@ $(TESTING_ACTION_EXE): tests/testingaction/main.go compat-tests: export KOPIA_CURRENT_EXE=$(CURDIR)/$(kopia_ui_embedded_exe) compat-tests: export KOPIA_08_EXE=$(kopia08) compat-tests: export KOPIA_017_EXE=$(kopia017) -compat-tests: GOTESTSUM_FLAGS=--format=testname --no-summary=skipped --jsonfile=.tmp.compat-tests.json +compat-tests: GOTESTSUM_FLAGS=--format=testname --no-summary=output --jsonfile=.tmp.compat-tests.json compat-tests: $(kopia_ui_embedded_exe) $(kopia08) $(kopia017) $(gotestsum) $(GO_TEST) $(TEST_FLAGS) -count=$(REPEAT_TEST) -parallel $(PARALLEL) -timeout 3600s github.com/kopia/kopia/tests/compat_test # -$(gotestsum) tool slowest --jsonfile .tmp.compat-tests.json --threshold 1000ms @@ -385,8 +393,9 @@ ifneq ($(GOOS),windows) -e github.com/kopia/kopia/issues && exit 1 || echo repo/ layering ok endif +htmlui-e2e-test: GOTESTSUM_FORMAT=testname htmlui-e2e-test: - HTMLUI_E2E_TEST=1 go test -timeout 600s github.com/kopia/kopia/tests/htmlui_e2e_test -v $(TEST_FLAGS) + HTMLUI_E2E_TEST=1 $(GO_TEST) -timeout 600s github.com/kopia/kopia/tests/htmlui_e2e_test -v $(TEST_FLAGS) htmlui-e2e-test-local-htmlui-changes: (cd ../htmlui && npm run build) diff --git a/README.md b/README.md index a4d09c63fe0..5222c202963 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Kopia ![Kopia](icons/kopia.svg) [![Build Status](https://github.com/kopia/kopia/workflows/Build/badge.svg)](https://github.com/kopia/kopia/actions?query=workflow%3ABuild) -[![Slack](https://img.shields.io/badge/discuss-slack-blue.svg)](https://slack.kopia.io/) [![GoDoc](https://godoc.org/github.com/kopia/kopia/repo?status.svg)](https://godoc.org/github.com/kopia/kopia/repo) [![Coverage Status](https://codecov.io/gh/kopia/kopia/branch/master/graph/badge.svg?token=CRK4RMRFSH)](https://codecov.io/gh/kopia/kopia)[![Go Report Card](https://goreportcard.com/badge/github.com/kopia/kopia)](https://goreportcard.com/report/github.com/kopia/kopia) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) @@ -58,22 +57,22 @@ Using Kopia via graphical user interface (note: the video is of an older version Getting Started --- -See [Kopia Documentation](https://kopia.io/docs/) for more information. - -Building Kopia ---- -See [Build Infrastructure](BUILD.md) for more information on building Kopia and working with the source code. +See [Kopia Documentation](https://kopia.io/docs/) for more information. Also check out the [users forum](https://kopia.discourse.group). Licensing --- Kopia is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text. +Building Kopia +--- +See [Build Infrastructure](BUILD.md) for more information on building Kopia and working with the source code. + Contribution Guidelines --- -Kopia is open source and contributions are welcome. For more information on how to contribute see the [Contribution Guidelines](https://kopia.io/docs/contribution-guidelines/). +Kopia is open source. For more information see the [Contribution Guidelines](https://kopia.io/docs/contribution-guidelines/). Reporting Security Issues --- -If you find a security issue you'd like to disclose privately, please contact `security@kopia.io` or via direct message to maintainers on [Slack](https://slack.kopia.io). +If you find a security issue you'd like to disclose privately, please contact `security@kopia.io`. [![Netlify Status](https://api.netlify.com/api/v1/badges/6b5c1fe4-a0da-4e7e-939b-ff1105251985/deploy-status)](https://app.netlify.com/sites/kopia/deploys) diff --git a/app/package-lock.json b/app/package-lock.json index e529b7f418a..7c401ab3655 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -10,24 +10,24 @@ "license": "Apache-2.0", "dependencies": { "auto-launch": "^5.0.6", - "electron-log": "^5.4.0", - "electron-store": "^10.0.1", - "electron-updater": "^6.6.2", + "electron-log": "^5.4.3", + "electron-store": "^11.0.2", + "electron-updater": "^6.8.3", "minimist": "^1.2.8", "semver": "^7.7.2", - "uuid": "^11.1.0" + "uuid": "^14.0.0" }, "devDependencies": { - "@electron/notarize": "^3.0.1", - "@playwright/test": "^1.52.0", + "@electron/notarize": "^3.1.1", + "@playwright/test": "^1.59.1", "asar": "^3.2.0", - "concurrently": "^9.1.2", - "dotenv": "^16.5.0", - "electron": "^36.8.1", - "electron-builder": "^26.0.12", + "concurrently": "^9.2.1", + "dotenv": "^17.4.2", + "electron": "^41.3.0", + "electron-builder": "^26.8.1", "playwright": "^1.37.1", "playwright-core": "^1.35.1", - "prettier": "^3.5.3" + "prettier": "^3.8.3" } }, "node_modules/@develar/schema-utils": { @@ -199,9 +199,9 @@ } }, "node_modules/@electron/notarize": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.0.1.tgz", - "integrity": "sha512-5xzcOwvMGNjkSk7s0sPx4XcKWei9FYk4f2S5NkSorWW0ce5yktTOtlPa0W5yQHcREILh+C3JdH+t+M637g9TmQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", + "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -295,9 +295,9 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -713,13 +713,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/plist": { @@ -1189,12 +1189,13 @@ } }, "node_modules/atomically": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", - "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "license": "MIT", "dependencies": { - "stubborn-fs": "^1.2.5", - "when-exit": "^2.1.1" + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" } }, "node_modules/auto-launch": { @@ -1263,9 +1264,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1341,9 +1342,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", - "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -1392,9 +1393,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -1695,19 +1696,18 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", @@ -1721,23 +1721,23 @@ } }, "node_modules/conf": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-13.1.0.tgz", - "integrity": "sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", "license": "MIT", "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", - "dot-prop": "^9.0.0", + "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", - "semver": "^7.6.3", - "uint8array-extras": "^1.4.0" + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2053,24 +2053,24 @@ "optional": true }, "node_modules/dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", "license": "MIT", "dependencies": { - "type-fest": "^4.18.2" + "type-fest": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2096,6 +2096,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2135,15 +2148,15 @@ } }, "node_modules/electron": { - "version": "36.8.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-36.8.1.tgz", - "integrity": "sha512-honaH58/cyCb9QAzIvD+WXWuNIZ0tW9zfBqMz5wZld/rXB+LCTEDb2B3TAv8+pDmlzPlkPio95RkUe86l6MNjg==", + "version": "41.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.3.0.tgz", + "integrity": "sha512-2Q5aeocmFdeheZGDUTrAvSR3t+n0c3d104AJWWEnt7syJU0tE4VdibMYaPtQ47QuXSoUf0/xSsfUUvu/uSXIfg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", + "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -2207,9 +2220,9 @@ } }, "node_modules/electron-log": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.0.tgz", - "integrity": "sha512-AXI5OVppskrWxEAmCxuv8ovX+s2Br39CpCAgkGMNHQtjYT3IiVbSQTncEjFVGPgoH35ZygRm/mvUMBDWwhRxgg==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -2247,13 +2260,13 @@ } }, "node_modules/electron-store": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-10.0.1.tgz", - "integrity": "sha512-Ok0bF13WWdTzZi9rCtPN8wUfwx+yDMmV6PAnCMqjNRKEXHmklW/rV+6DofV/Vf5qoAh+Bl9Bj7dQ+0W+IL2psg==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-11.0.2.tgz", + "integrity": "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ==", "license": "MIT", "dependencies": { - "conf": "^13.0.0", - "type-fest": "^4.20.0" + "conf": "^15.0.2", + "type-fest": "^5.0.1" }, "engines": { "node": ">=20" @@ -2263,18 +2276,18 @@ } }, "node_modules/electron-updater": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz", - "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", + "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", "license": "MIT", "dependencies": { - "builder-util-runtime": "9.3.1", + "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", - "semver": "^7.6.3", + "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, @@ -2517,9 +2530,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -2571,9 +2584,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -3213,9 +3226,9 @@ "license": "MIT" }, "node_modules/json-schema-typed": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", - "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, "node_modules/json-stringify-safe": { @@ -3979,9 +3992,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -4272,9 +4285,9 @@ "license": "ISC" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4346,9 +4359,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -4556,9 +4569,19 @@ } }, "node_modules/stubborn-fs": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", - "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" }, "node_modules/sumchecker": { "version": "3.0.1", @@ -4733,21 +4756,24 @@ "license": "0BSD" }, "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", "license": "MIT", "engines": { "node": ">=18" @@ -4757,9 +4783,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -4832,16 +4858,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/verror": { @@ -4871,9 +4897,9 @@ } }, "node_modules/when-exit": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz", - "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "license": "MIT" }, "node_modules/which": { diff --git a/app/package.json b/app/package.json index bd3a699a5d0..f45b83b005b 100644 --- a/app/package.json +++ b/app/package.json @@ -4,12 +4,12 @@ "repository": "github:kopia/kopia", "dependencies": { "auto-launch": "^5.0.6", - "electron-log": "^5.4.0", - "electron-store": "^10.0.1", - "electron-updater": "^6.6.2", + "electron-log": "^5.4.3", + "electron-store": "^11.0.2", + "electron-updater": "^6.8.3", "minimist": "^1.2.8", "semver": "^7.7.2", - "uuid": "^11.1.0" + "uuid": "^14.0.0" }, "type": "module", "author": { @@ -122,16 +122,16 @@ "appArmorProfile": "kopia-ui.apparmor" }, "devDependencies": { - "@electron/notarize": "^3.0.1", - "@playwright/test": "^1.52.0", + "@electron/notarize": "^3.1.1", + "@playwright/test": "^1.59.1", "asar": "^3.2.0", - "concurrently": "^9.1.2", - "dotenv": "^16.5.0", - "electron": "^36.8.1", - "electron-builder": "^26.0.12", + "concurrently": "^9.2.1", + "dotenv": "^17.4.2", + "electron": "^41.3.0", + "electron-builder": "^26.8.1", "playwright": "^1.37.1", "playwright-core": "^1.35.1", - "prettier": "^3.5.3" + "prettier": "^3.8.3" }, "homepage": "./", "description": "Fast and secure open source backup.", diff --git a/cli/app.go b/cli/app.go index 3c6b5bf1b07..42d7f8d55e2 100644 --- a/cli/app.go +++ b/cli/app.go @@ -325,21 +325,8 @@ type commandParent interface { // NewApp creates a new instance of App. func NewApp() *App { return &App{ - progress: &cliProgress{}, - cliStorageProviders: []StorageProvider{ - {"from-config", "the provided configuration file", func() StorageFlags { return &storageFromConfigFlags{} }}, - - {"azure", "an Azure blob storage", func() StorageFlags { return &storageAzureFlags{} }}, - {"b2", "a B2 bucket", func() StorageFlags { return &storageB2Flags{} }}, - {"filesystem", "a filesystem", func() StorageFlags { return &storageFilesystemFlags{} }}, - {"gcs", "a Google Cloud Storage bucket", func() StorageFlags { return &storageGCSFlags{} }}, - {"gdrive", "a Google Drive folder", func() StorageFlags { return &storageGDriveFlags{} }}, - - {"rclone", "a rclone-based provided", func() StorageFlags { return &storageRcloneFlags{} }}, - {"s3", "an S3 bucket", func() StorageFlags { return &storageS3Flags{} }}, - {"sftp", "an SFTP storage", func() StorageFlags { return &storageSFTPFlags{} }}, - {"webdav", "a WebDAV storage", func() StorageFlags { return &storageWebDAVFlags{} }}, - }, + progress: &cliProgress{}, + cliStorageProviders: getRegisteredStorageProviders(), // testability hooks exitWithError: func(err error) { diff --git a/cli/cli_progress.go b/cli/cli_progress.go index ce7626ef4eb..02a1d5170be 100644 --- a/cli/cli_progress.go +++ b/cli/cli_progress.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "os" "strconv" "strings" "sync" @@ -10,6 +11,7 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/fatih/color" + "golang.org/x/term" "github.com/kopia/kopia/internal/timetrack" "github.com/kopia/kopia/internal/units" @@ -29,7 +31,13 @@ type progressFlags struct { } func (p *progressFlags) setup(svc appServices, app *kingpin.Application) { - app.Flag("progress", "Enable progress output").Default("true").BoolVar(&p.enableProgress) + progressDefault := "false" + + if fd, err := intFd(os.Stdout); err == nil && term.IsTerminal(fd) { + progressDefault = "true" + } + + app.Flag("progress", "Enable progress output").Default(progressDefault).BoolVar(&p.enableProgress) app.Flag("progress-estimation-type", "Set type of estimation of the data to be snapshotted").Hidden().Default(upload.EstimationTypeClassic). EnumVar(&p.progressEstimationType, upload.EstimationTypeClassic, upload.EstimationTypeRough, upload.EstimationTypeAdaptive) app.Flag("progress-update-interval", "How often to update progress information").Hidden().Default("300ms").DurationVar(&p.progressUpdateInterval) diff --git a/cli/command_benchmark.go b/cli/command_benchmark.go index 447926b59cc..714430d0bbc 100644 --- a/cli/command_benchmark.go +++ b/cli/command_benchmark.go @@ -31,7 +31,11 @@ type cryptoBenchResult struct { throughput float64 } -func runInParallelNoInputNoResult(n int, run func()) { +func runInParallelNoInputNoResult(n uint, run func()) { + if n == 0 { + return + } + dummyArgs := make([]int, n) runInParallelNoResult(dummyArgs, func(_ int) { @@ -39,7 +43,13 @@ func runInParallelNoInputNoResult(n int, run func()) { }) } -func runInParallelNoInput[T any](n int, run func() T) T { +func runInParallelNoInput[T any](n uint, run func() T) T { + if n == 0 { + var zero T + + return zero + } + dummyArgs := make([]int, n) return runInParallel(dummyArgs, func(_ int) T { diff --git a/cli/command_benchmark_crypto.go b/cli/command_benchmark_crypto.go index dda48ef4d81..ddba8dedf90 100644 --- a/cli/command_benchmark_crypto.go +++ b/cli/command_benchmark_crypto.go @@ -16,10 +16,10 @@ import ( type commandBenchmarkCrypto struct { blockSize atunits.Base2Bytes - repeat int + repeat uint deprecatedAlgorithms bool optionPrint bool - parallel int + parallel uint out textOutput } @@ -27,9 +27,9 @@ type commandBenchmarkCrypto struct { func (c *commandBenchmarkCrypto) setup(svc appServices, parent commandParent) { cmd := parent.Command("crypto", "Run combined hash and encryption benchmarks") cmd.Flag("block-size", "Size of a block to encrypt").Default("1MB").BytesVar(&c.blockSize) - cmd.Flag("repeat", "Number of repetitions").Default("100").IntVar(&c.repeat) + cmd.Flag("repeat", "Number of repetitions").Default("100").UintVar(&c.repeat) cmd.Flag("deprecated", "Include deprecated algorithms").BoolVar(&c.deprecatedAlgorithms) - cmd.Flag("parallel", "Number of parallel goroutines").Default("1").IntVar(&c.parallel) + cmd.Flag("parallel", "Number of parallel goroutines").Default("1").UintVar(&c.parallel) cmd.Flag("print-options", "Print out options usable for repository creation").BoolVar(&c.optionPrint) cmd.Action(svc.noRepositoryAction(c.run)) c.out.setup(svc) diff --git a/cli/command_benchmark_ecc.go b/cli/command_benchmark_ecc.go index 59e90dc89f8..addf8b75f75 100644 --- a/cli/command_benchmark_ecc.go +++ b/cli/command_benchmark_ecc.go @@ -16,9 +16,9 @@ import ( type commandBenchmarkEcc struct { blockSize atunits.Base2Bytes - repeat int + repeat uint optionPrint bool - parallel int + parallel uint out textOutput } @@ -26,8 +26,8 @@ type commandBenchmarkEcc struct { func (c *commandBenchmarkEcc) setup(svc appServices, parent commandParent) { cmd := parent.Command("ecc", "Run ECC benchmarks") cmd.Flag("block-size", "Size of a block to encrypt").Default("10MB").BytesVar(&c.blockSize) - cmd.Flag("repeat", "Number of repetitions").Default("100").IntVar(&c.repeat) - cmd.Flag("parallel", "Number of parallel goroutines").Default("1").IntVar(&c.parallel) + cmd.Flag("repeat", "Number of repetitions").Default("100").UintVar(&c.repeat) + cmd.Flag("parallel", "Number of parallel goroutines").Default("1").UintVar(&c.parallel) cmd.Flag("print-options", "Print out options usable for repository creation").BoolVar(&c.optionPrint) cmd.Action(svc.noRepositoryAction(c.run)) c.out.setup(svc) diff --git a/cli/command_benchmark_encryption.go b/cli/command_benchmark_encryption.go index aa3362420ad..9a62dc1e32f 100644 --- a/cli/command_benchmark_encryption.go +++ b/cli/command_benchmark_encryption.go @@ -16,10 +16,10 @@ import ( type commandBenchmarkEncryption struct { blockSize atunits.Base2Bytes - repeat int + repeat uint deprecatedAlgorithms bool optionPrint bool - parallel int + parallel uint out textOutput } @@ -27,9 +27,9 @@ type commandBenchmarkEncryption struct { func (c *commandBenchmarkEncryption) setup(svc appServices, parent commandParent) { cmd := parent.Command("encryption", "Run encryption benchmarks") cmd.Flag("block-size", "Size of a block to encrypt").Default("1MB").BytesVar(&c.blockSize) - cmd.Flag("repeat", "Number of repetitions").Default("1000").IntVar(&c.repeat) + cmd.Flag("repeat", "Number of repetitions").Default("1000").UintVar(&c.repeat) cmd.Flag("deprecated", "Include deprecated algorithms").BoolVar(&c.deprecatedAlgorithms) - cmd.Flag("parallel", "Number of parallel goroutines").Default("1").IntVar(&c.parallel) + cmd.Flag("parallel", "Number of parallel goroutines").Default("1").UintVar(&c.parallel) cmd.Flag("print-options", "Print out options usable for repository creation").BoolVar(&c.optionPrint) cmd.Action(svc.noRepositoryAction(c.run)) c.out.setup(svc) diff --git a/cli/command_benchmark_hashing.go b/cli/command_benchmark_hashing.go index a1c2f31ceb0..36fa79cbc54 100644 --- a/cli/command_benchmark_hashing.go +++ b/cli/command_benchmark_hashing.go @@ -15,9 +15,9 @@ import ( type commandBenchmarkHashing struct { blockSize atunits.Base2Bytes - repeat int + repeat uint optionPrint bool - parallel int + parallel uint out textOutput } @@ -25,8 +25,8 @@ type commandBenchmarkHashing struct { func (c *commandBenchmarkHashing) setup(svc appServices, parent commandParent) { cmd := parent.Command("hashing", "Run hashing function benchmarks").Alias("hash") cmd.Flag("block-size", "Size of a block to hash").Default("1MB").BytesVar(&c.blockSize) - cmd.Flag("repeat", "Number of repetitions").Default("10").IntVar(&c.repeat) - cmd.Flag("parallel", "Number of parallel goroutines").Default("1").IntVar(&c.parallel) + cmd.Flag("repeat", "Number of repetitions").Default("100").UintVar(&c.repeat) + cmd.Flag("parallel", "Number of parallel goroutines").Default("1").UintVar(&c.parallel) cmd.Flag("print-options", "Print out options usable for repository creation").BoolVar(&c.optionPrint) cmd.Action(svc.noRepositoryAction(c.run)) c.out.setup(svc) @@ -82,9 +82,7 @@ func (c *commandBenchmarkHashing) runBenchmark(ctx context.Context) []cryptoBenc var hashOutput [hashing.MaxHashSize]byte for range hashCount { - for range hashOutput { - hf(hashOutput[:0], input) - } + hf(hashOutput[:0], input) } }) diff --git a/cli/command_benchmark_splitters.go b/cli/command_benchmark_splitters.go index f2fdb0df4e4..be75dfa06bc 100644 --- a/cli/command_benchmark_splitters.go +++ b/cli/command_benchmark_splitters.go @@ -19,9 +19,9 @@ import ( type commandBenchmarkSplitters struct { randSeed int64 blockSize atunits.Base2Bytes - blockCount int + blockCount uint printOption bool - parallel int + parallel uint out textOutput } @@ -31,9 +31,9 @@ func (c *commandBenchmarkSplitters) setup(svc appServices, parent commandParent) cmd.Flag("rand-seed", "Random seed").Default("42").Int64Var(&c.randSeed) cmd.Flag("data-size", "Size of a data to split").Default("32MB").BytesVar(&c.blockSize) - cmd.Flag("block-count", "Number of data blocks to split").Default("16").IntVar(&c.blockCount) + cmd.Flag("block-count", "Number of data blocks to split").Default("16").UintVar(&c.blockCount) cmd.Flag("print-options", "Print out the fastest dynamic splitter option").BoolVar(&c.printOption) - cmd.Flag("parallel", "Number of parallel goroutines").Default("1").IntVar(&c.parallel) + cmd.Flag("parallel", "Number of parallel goroutines").Default("1").UintVar(&c.parallel) cmd.Action(svc.noRepositoryAction(c.run)) diff --git a/cli/command_repository_connect_from_config.go b/cli/command_repository_connect_from_config.go index 56e215f7ca4..f48d68a65e5 100644 --- a/cli/command_repository_connect_from_config.go +++ b/cli/command_repository_connect_from_config.go @@ -101,3 +101,11 @@ func (c *storageFromConfigFlags) connectToStorageFromStorageConfigStdin(ctx cont return c.connectToStorageFromConfigToken(ctx, string(tokenData)) } + +func init() { + mustRegisterStorageProvider( + "from-config", + "the provided configuration file", + func() StorageFlags { return &storageFromConfigFlags{} }, + ) +} diff --git a/cli/command_repository_set_parameters.go b/cli/command_repository_set_parameters.go index 5baf3399d35..df61c37a499 100644 --- a/cli/command_repository_set_parameters.go +++ b/cli/command_repository_set_parameters.go @@ -54,7 +54,7 @@ func (c *commandRepositorySetParameters) setup(svc appServices, parent commandPa cmd.Flag("epoch-advance-on-count", "Advance epoch if the number of indexes exceeds given threshold").IntVar(&c.epochAdvanceOnCount) cmd.Flag("epoch-advance-on-size-mb", "Advance epoch if the total size of indexes exceeds given threshold").Int64Var(&c.epochAdvanceOnSizeMB) cmd.Flag("epoch-delete-parallelism", "Epoch delete parallelism").IntVar(&c.epochDeleteParallelism) - cmd.Flag("epoch-checkpoint-frequency", "Checkpoint frequency").IntVar(&c.epochCheckpointFrequency) + cmd.Flag("epoch-checkpoint-frequency", "Epoch range-compaction period").PlaceHolder("number-of-epochs").IntVar(&c.epochCheckpointFrequency) if svc.enableTestOnlyFlags() { cmd.Flag("add-required-feature", "Add required feature which must be present to open the repository").Hidden().StringVar(&c.addRequiredFeature) @@ -212,7 +212,7 @@ func (c *commandRepositorySetParameters) run(ctx context.Context, rep repo.Direc setIntParameter(ctx, c.epochAdvanceOnCount, "epoch advance on count", &mp.EpochParameters.EpochAdvanceOnCountThreshold, &anyChange) setSizeMBParameter(ctx, c.epochAdvanceOnSizeMB, "epoch advance on total size", &mp.EpochParameters.EpochAdvanceOnTotalSizeBytesThreshold, &anyChange) setIntParameter(ctx, c.epochDeleteParallelism, "epoch delete parallelism", &mp.EpochParameters.DeleteParallelism, &anyChange) - setIntParameter(ctx, c.epochCheckpointFrequency, "epoch checkpoint frequency", &mp.EpochParameters.FullCheckpointFrequency, &anyChange) + setIntParameter(ctx, c.epochCheckpointFrequency, "epoch range-compaction period", &mp.EpochParameters.FullCheckpointFrequency, &anyChange) requiredFeatures = c.addRemoveUpdateRequiredFeatures(requiredFeatures, &anyChange) diff --git a/cli/command_repository_set_parameters_test.go b/cli/command_repository_set_parameters_test.go index 68dfa5ae259..425b8bbc0b0 100644 --- a/cli/command_repository_set_parameters_test.go +++ b/cli/command_repository_set_parameters_test.go @@ -184,7 +184,7 @@ func (s *formatSpecificTestSuite) TestRepositorySetParametersUpgrade(t *testing. require.Contains(t, out, "Format version: 3") require.Contains(t, out, "Epoch cleanup margin: 23h0m0s") require.Contains(t, out, "Epoch advance on: 22 blobs or 80.7 MB, minimum 3h0m0s") - require.Contains(t, out, "Epoch checkpoint every: 9 epochs") + require.Contains(t, out, "Epoch range-compaction every: 9 epochs") env.RunAndExpectSuccess(t, "index", "epoch", "list") } diff --git a/cli/command_repository_status.go b/cli/command_repository_status.go index 7eb2cf22212..a7a813f3db1 100644 --- a/cli/command_repository_status.go +++ b/cli/command_repository_status.go @@ -211,7 +211,7 @@ func (c *commandRepositoryStatus) run(ctx context.Context, rep repo.Repository) c.out.printStdout("Epoch refresh frequency: %v\n", mp.EpochParameters.EpochRefreshFrequency) c.out.printStdout("Epoch advance on: %v blobs or %v, minimum %v\n", mp.EpochParameters.EpochAdvanceOnCountThreshold, units.BytesString(mp.EpochParameters.EpochAdvanceOnTotalSizeBytesThreshold), mp.EpochParameters.MinEpochDuration) c.out.printStdout("Epoch cleanup margin: %v\n", mp.EpochParameters.CleanupSafetyMargin) - c.out.printStdout("Epoch checkpoint every: %v epochs\n", mp.EpochParameters.FullCheckpointFrequency) + c.out.printStdout("Epoch range-compaction every: %v epochs\n", mp.EpochParameters.FullCheckpointFrequency) } else { c.out.printStdout("Epoch Manager: disabled\n") } diff --git a/cli/command_server_start.go b/cli/command_server_start.go index 5f00dac10b3..0bae249e953 100644 --- a/cli/command_server_start.go +++ b/cli/command_server_start.go @@ -20,6 +20,7 @@ import ( htpasswd "github.com/tg123/go-htpasswd" "github.com/kopia/kopia/internal/auth" + "github.com/kopia/kopia/internal/insecureserverbind" "github.com/kopia/kopia/internal/server" "github.com/kopia/kopia/notification" "github.com/kopia/kopia/notification/sender/jsonsender" @@ -75,6 +76,8 @@ type commandServerStart struct { logServerRequests bool + serverStartAllowDangerousUnauthenticatedNetwork bool + disableCSRFTokenChecks bool // disable CSRF token checks - used for development/debugging only sf serverFlags @@ -94,6 +97,10 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent cmd.Flag("insecure", "Allow insecure configurations (do not use in production)").Hidden().BoolVar(&c.serverStartInsecure) cmd.Flag("max-concurrency", "Maximum number of server goroutines").Default("0").IntVar(&c.serverStartMaxConcurrency) + cmd.Flag(insecureserverbind.AllowDangerousUnauthenticatedNetworkFlag, insecureserverbind.AllowDangerousUnauthenticatedNetworkFlagHelp). + Hidden(). + BoolVar(&c.serverStartAllowDangerousUnauthenticatedNetwork) + cmd.Flag("without-password", "Start the server without a password").Hidden().BoolVar(&c.serverStartWithoutPassword) cmd.Flag("random-password", "Generate random password and print to stderr").Hidden().BoolVar(&c.serverStartRandomPassword) cmd.Flag("htpasswd-file", "Path to htpasswd file that contains allowed user@hostname entries").Hidden().ExistingFileVar(&c.serverStartHtpasswdFile) @@ -190,6 +197,15 @@ func (c *commandServerStart) initRepositoryPossiblyAsync(ctx context.Context, sr } func (c *commandServerStart) run(ctx context.Context) (reterr error) { + if err := insecureserverbind.ValidateListenAddressIfRestricted( + c.serverStartInsecure, + c.serverStartWithoutPassword, + c.serverStartAllowDangerousUnauthenticatedNetwork, + c.sf.serverAddress, + ); err != nil { + return errors.Wrap(err, "listen address not allowed for insecure server without password") + } + opts, err := c.serverStartOptions(ctx) if err != nil { return err diff --git a/cli/command_server_tls.go b/cli/command_server_tls.go index 99cc06ba5c3..bf30b566559 100644 --- a/cli/command_server_tls.go +++ b/cli/command_server_tls.go @@ -20,6 +20,7 @@ import ( "github.com/coreos/go-systemd/v22/activation" "github.com/pkg/errors" + "github.com/kopia/kopia/internal/insecureserverbind" "github.com/kopia/kopia/internal/tlsutil" ) @@ -62,6 +63,17 @@ func (c *commandServerStart) startServerWithOptionalTLS(ctx context.Context, htt return errors.Errorf("Too many activated sockets found. Expected 1, got %v", len(listeners)) } + if err := insecureserverbind.ValidateListenerAddrIfRestricted( + c.serverStartInsecure, + c.serverStartWithoutPassword, + c.serverStartAllowDangerousUnauthenticatedNetwork, + l.Addr(), + ); err != nil { + l.Close() //nolint:errcheck + + return errors.Wrap(err, "insecure server bind validation") + } + defer l.Close() //nolint:errcheck httpServer.Addr = l.Addr().String() diff --git a/cli/error_notifications.go b/cli/error_notifications.go index 4061b0c13c2..40e92c1a0d7 100644 --- a/cli/error_notifications.go +++ b/cli/error_notifications.go @@ -3,7 +3,7 @@ package cli import ( "os" - "github.com/mattn/go-isatty" + "golang.org/x/term" ) const ( @@ -25,7 +25,7 @@ func (c *App) enableErrorNotifications() bool { return false } - if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { + if fd, err := intFd(os.Stdout); err == nil && term.IsTerminal(fd) { // interactive terminal, don't send notifications return false } diff --git a/cli/observability_flags.go b/cli/observability_flags.go index 442d59acdde..b75637baee9 100644 --- a/cli/observability_flags.go +++ b/cli/observability_flags.go @@ -29,8 +29,8 @@ import ( "github.com/kopia/kopia/repo" ) -// DirMode is the directory mode for output directories. -const DirMode = 0o700 +// dirMode is the directory mode for output directories. +const dirMode = 0o700 //nolint:gochecknoglobals var metricsPushFormats = map[string]expfmt.Format{ @@ -165,7 +165,7 @@ func (c *observabilityFlags) start(ctx context.Context) error { func mkSubdirectories(directoryNames ...string) (dirName string, err error) { dirName = filepath.Join(directoryNames...) - if err := os.MkdirAll(dirName, DirMode); err != nil { + if err := os.MkdirAll(dirName, dirMode); err != nil { return "", errors.Wrapf(err, "could not create '%q' subdirectory to save diagnostics output", dirName) } diff --git a/cli/password.go b/cli/password.go index e205a6f33e8..d452e7e7f8e 100644 --- a/cli/password.go +++ b/cli/password.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "math" "os" "strings" @@ -94,10 +95,15 @@ func (c *App) getPasswordFromFlags(ctx context.Context, isCreate, allowPersisten // askPass presents a given prompt and asks the user for password. func askPass(out io.Writer, prompt string) (string, error) { + fd, err := intFd(os.Stdin) + if err != nil { + return "", errors.Wrap(err, "password input error") + } + for range 5 { fmt.Fprint(out, prompt) //nolint:errcheck - passBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + passBytes, err := term.ReadPassword(fd) if err != nil { return "", errors.Wrap(err, "password prompt error") } @@ -113,3 +119,15 @@ func askPass(out io.Writer, prompt string) (string, error) { return "", errors.New("can't get password") } + +var errFdConversionOverflows = errors.New("uintptr file descriptor conversion to int overflows") + +func intFd(f *os.File) (int, error) { + fd := f.Fd() + + if fd <= math.MaxInt { + return int(fd), nil + } + + return -1, errFdConversionOverflows +} diff --git a/cli/storage_azure.go b/cli/storage_azure.go index 14abda32b6d..68bcb37e59b 100644 --- a/cli/storage_azure.go +++ b/cli/storage_azure.go @@ -58,3 +58,11 @@ func (c *storageAzureFlags) Connect(ctx context.Context, isCreate bool, formatVe //nolint:wrapcheck return azure.New(ctx, &c.azOptions, isCreate) } + +func init() { + mustRegisterStorageProvider( + "azure", + "an Azure blob storage", + func() StorageFlags { return &storageAzureFlags{} }, + ) +} diff --git a/cli/storage_b2.go b/cli/storage_b2.go index 6d0273e7431..e2506c9f1d4 100644 --- a/cli/storage_b2.go +++ b/cli/storage_b2.go @@ -27,3 +27,11 @@ func (c *storageB2Flags) Connect(ctx context.Context, isCreate bool, formatVersi //nolint:wrapcheck return b2.New(ctx, &c.b2options, isCreate) } + +func init() { + mustRegisterStorageProvider( + "b2", + "a B2 bucket [DEPRECATED]", + func() StorageFlags { return &storageB2Flags{} }, + ) +} diff --git a/cli/storage_filesystem.go b/cli/storage_filesystem.go index bfd10b4baa5..c0394104230 100644 --- a/cli/storage_filesystem.go +++ b/cli/storage_filesystem.go @@ -97,3 +97,11 @@ func getFileModeValue(value string, def os.FileMode) os.FileMode { return def } + +func init() { + mustRegisterStorageProvider( + "filesystem", + "a filesystem", + func() StorageFlags { return &storageFilesystemFlags{} }, + ) +} diff --git a/cli/storage_gcs.go b/cli/storage_gcs.go index 707407aad6b..173ff86dd45 100644 --- a/cli/storage_gcs.go +++ b/cli/storage_gcs.go @@ -66,3 +66,11 @@ func (c *storageGCSFlags) Connect(ctx context.Context, isCreate bool, formatVers //nolint:wrapcheck return gcs.New(ctx, &c.options, isCreate) } + +func init() { + mustRegisterStorageProvider( + "gcs", + "a Google Cloud Storage bucket", + func() StorageFlags { return &storageGCSFlags{} }, + ) +} diff --git a/cli/storage_gdrive.go b/cli/storage_gdrive.go index 1ff81b8065e..d41c5a137fe 100644 --- a/cli/storage_gdrive.go +++ b/cli/storage_gdrive.go @@ -43,3 +43,11 @@ func (c *storageGDriveFlags) Connect(ctx context.Context, isCreate bool, formatV //nolint:wrapcheck return gdrive.New(ctx, &c.options, isCreate) } + +func init() { + mustRegisterStorageProvider( + "gdrive", + "a Google Drive folder [Not maintained]", + func() StorageFlags { return &storageGDriveFlags{} }, + ) +} diff --git a/cli/storage_providers.go b/cli/storage_providers.go index 67a720263ff..c58ef8940fe 100644 --- a/cli/storage_providers.go +++ b/cli/storage_providers.go @@ -3,9 +3,14 @@ package cli import ( "context" "io" + "maps" + "slices" + "sync" "github.com/alecthomas/kingpin/v2" + "github.com/pkg/errors" + "github.com/kopia/kopia/internal/impossible" "github.com/kopia/kopia/repo/blob" "github.com/kopia/kopia/repo/blob/throttling" ) @@ -35,6 +40,58 @@ type StorageProvider struct { NewFlags func() StorageFlags } +//nolint:gochecknoglobals +var ( + registeredProvidersMu sync.Mutex + // +checklocks:registeredProvidersMu + registeredProviders = make(map[string]StorageProvider) + + errStorageAlreadyRegistered = errors.New("storage provider already registered") // +checklocksignore +) + +// mustRegisterStorageProvider registers a storage provider for use with the CLI repository connect command. +// It should typically be called from init() functions in storage provider packages. +// It panics if the storage provider has already been registered. +func mustRegisterStorageProvider(name, description string, newFlags func() StorageFlags) { + impossible.PanicOnError(registerStorageProvider(name, description, newFlags)) +} + +func registerStorageProvider(name, description string, newFlags func() StorageFlags) error { + registeredProvidersMu.Lock() + defer registeredProvidersMu.Unlock() + + if _, ok := registeredProviders[name]; ok { + return errors.Wrapf(errStorageAlreadyRegistered, "%s", name) + } + + registeredProviders[name] = StorageProvider{ + Name: name, + Description: description, + NewFlags: newFlags, + } + + return nil +} + +// getRegisteredStorageProviders returns a copy of all registered storage providers. +// This is used internally by the App to build the list of available storage providers. +func getRegisteredStorageProviders() []StorageProvider { + registeredProvidersMu.Lock() + defer registeredProvidersMu.Unlock() + + // Return a copy to prevent external modification + p := make([]StorageProvider, 0, len(registeredProviders)) + for _, n := range slices.Sorted(maps.Keys(registeredProviders)) { + p = append(p, registeredProviders[n]) + } + + if len(p) != len(registeredProviders) { + panic("expected provider length mismatch") + } + + return p +} + func commonThrottlingFlags(cmd *kingpin.CmdClause, limits *throttling.Limits) { cmd.Flag("max-download-speed", "Limit the download speed.").PlaceHolder("BYTES_PER_SEC").FloatVar(&limits.DownloadBytesPerSecond) cmd.Flag("max-upload-speed", "Limit the upload speed.").PlaceHolder("BYTES_PER_SEC").FloatVar(&limits.UploadBytesPerSecond) diff --git a/cli/storage_rclone.go b/cli/storage_rclone.go index b289825cf6e..38502fa232c 100644 --- a/cli/storage_rclone.go +++ b/cli/storage_rclone.go @@ -48,3 +48,11 @@ func (c *storageRcloneFlags) Connect(ctx context.Context, isCreate bool, formatV //nolint:wrapcheck return rclone.New(ctx, &c.opt, isCreate) } + +func init() { + mustRegisterStorageProvider( + "rclone", + "a rclone-based provider [Not maintained]", + func() StorageFlags { return &storageRcloneFlags{} }, + ) +} diff --git a/cli/storage_s3.go b/cli/storage_s3.go index 89f947c8733..3325017bd09 100644 --- a/cli/storage_s3.go +++ b/cli/storage_s3.go @@ -89,3 +89,11 @@ func (c *storageS3Flags) Connect(ctx context.Context, isCreate bool, formatVersi //nolint:wrapcheck return s3.New(ctx, &c.s3options, isCreate) } + +func init() { + mustRegisterStorageProvider( + "s3", + "an S3 bucket", + func() StorageFlags { return &storageS3Flags{} }, + ) +} diff --git a/cli/storage_sftp.go b/cli/storage_sftp.go index 4ede083cbf2..754a9c476b3 100644 --- a/cli/storage_sftp.go +++ b/cli/storage_sftp.go @@ -118,3 +118,11 @@ func (c *storageSFTPFlags) Connect(ctx context.Context, isCreate bool, formatVer //nolint:wrapcheck return sftp.New(ctx, opt, isCreate) } + +func init() { + mustRegisterStorageProvider( + "sftp", + "an SFTP storage", + func() StorageFlags { return &storageSFTPFlags{} }, + ) +} diff --git a/cli/storage_webdav.go b/cli/storage_webdav.go index f86dd22ea34..01d0b464ae4 100644 --- a/cli/storage_webdav.go +++ b/cli/storage_webdav.go @@ -43,3 +43,11 @@ func (c *storageWebDAVFlags) Connect(ctx context.Context, isCreate bool, formatV //nolint:wrapcheck return webdav.New(ctx, &wo, isCreate) } + +func init() { + mustRegisterStorageProvider( + "webdav", + "a WebDAV storage", + func() StorageFlags { return &storageWebDAVFlags{} }, + ) +} diff --git a/fs/localfs/local_fs.go b/fs/localfs/local_fs.go index d0ce32376ee..ed89abdbe19 100644 --- a/fs/localfs/local_fs.go +++ b/fs/localfs/local_fs.go @@ -13,16 +13,6 @@ import ( const numEntriesToRead = 100 // number of directory entries to read in one shot -// Options contains configuration options for localfs operations. -type Options struct { - // IgnoreUnreadableDirEntries, when true, causes unreadable directory entries - // to be silently skipped during directory iteration instead of causing errors. - IgnoreUnreadableDirEntries bool -} - -// DefaultOptions stores the default options used by localfs functions. -var DefaultOptions = &Options{} - type filesystemEntry struct { name string size int64 @@ -31,8 +21,7 @@ type filesystemEntry struct { owner fs.OwnerInfo device fs.DeviceInfo - prefix string - options *Options + prefix string } func (e *filesystemEntry) Name() string { @@ -103,7 +92,6 @@ func (fsd *filesystemDirectory) Size() int64 { type fileWithMetadata struct { *os.File - options *Options } func (f *fileWithMetadata) Entry() (fs.Entry, error) { @@ -114,7 +102,7 @@ func (f *fileWithMetadata) Entry() (fs.Entry, error) { basename, prefix := splitDirPrefix(f.Name()) - return newFilesystemFile(newEntry(basename, fi, prefix, f.options)), nil + return newFilesystemFile(newEntry(basename, fi, prefix)), nil } func (fsf *filesystemFile) Open(_ context.Context) (fs.Reader, error) { @@ -123,7 +111,7 @@ func (fsf *filesystemFile) Open(_ context.Context) (fs.Reader, error) { return nil, errors.Wrap(err, "unable to open local file") } - return &fileWithMetadata{File: f, options: fsf.options}, nil + return &fileWithMetadata{f}, nil } func (fsl *filesystemSymlink) Readlink(_ context.Context) (string, error) { @@ -137,7 +125,7 @@ func (fsl *filesystemSymlink) Resolve(_ context.Context) (fs.Entry, error) { return nil, errors.Wrapf(err, "cannot resolve symlink for '%q'", fsl.fullPath()) } - return NewEntryWithOptions(target, fsl.options) + return NewEntry(target) } func (e *filesystemErrorEntry) ErrorInfo() error { @@ -157,15 +145,8 @@ func splitDirPrefix(s string) (basename, prefix string) { } // Directory returns fs.Directory for the specified path. -// It uses DefaultOptions for configuration. func Directory(path string) (fs.Directory, error) { - return DirectoryWithOptions(path, DefaultOptions) -} - -// DirectoryWithOptions returns fs.Directory for the specified path. -// It uses the provided Options for configuration. -func DirectoryWithOptions(path string, options *Options) (fs.Directory, error) { - e, err := NewEntryWithOptions(path, options) + e, err := NewEntry(path) if err != nil { return nil, err } diff --git a/fs/localfs/local_fs_os.go b/fs/localfs/local_fs_os.go index cfc410d1d7a..97b13ea65b3 100644 --- a/fs/localfs/local_fs_os.go +++ b/fs/localfs/local_fs_os.go @@ -18,7 +18,6 @@ const separatorStr = string(filepath.Separator) type filesystemDirectoryIterator struct { dirHandle *os.File childPrefix string - options *Options currentIndex int currentBatch []os.DirEntry @@ -46,7 +45,7 @@ func (it *filesystemDirectoryIterator) Next(_ context.Context) (fs.Entry, error) n := it.currentIndex it.currentIndex++ - e, err := toDirEntryOrNil(it.currentBatch[n], it.childPrefix, it.options) + e, err := toDirEntryOrNil(it.currentBatch[n], it.childPrefix) if err != nil { // stop iteration return nil, err @@ -75,7 +74,7 @@ func (fsd *filesystemDirectory) Iterate(_ context.Context) (fs.DirectoryIterator childPrefix := fullPath + separatorStr - return &filesystemDirectoryIterator{dirHandle: d, childPrefix: childPrefix, options: fsd.options}, nil + return &filesystemDirectoryIterator{dirHandle: d, childPrefix: childPrefix}, nil } func (fsd *filesystemDirectory) Child(_ context.Context, name string) (fs.Entry, error) { @@ -90,39 +89,47 @@ func (fsd *filesystemDirectory) Child(_ context.Context, name string) (fs.Entry, return nil, errors.Wrap(err, "unable to get child") } - return entryFromDirEntry(name, st, fullPath+separatorStr, fsd.options), nil + return entryFromDirEntry(name, st, fullPath+separatorStr), nil } -func toDirEntryOrNil(dirEntry os.DirEntry, prefix string, options *Options) (fs.Entry, error) { +func toDirEntryOrNil(dirEntry os.DirEntry, prefix string) (fs.Entry, error) { n := dirEntry.Name() - fi, err := os.Lstat(prefix + n) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - - if options != nil && options.IgnoreUnreadableDirEntries { - return nil, nil + switch fi, err := os.Lstat(prefix + n); { + case err == nil: + return entryFromDirEntry(n, fi, prefix), nil + case os.IsNotExist(err): + return nil, nil + case os.IsPermission(err): + // For permission denied errors, return an ErrorEntry instead of failing + // the entire directory iteration. This allows the upload process to + // handle the error according to the configured error handling policy + // and continue processing other entries in the directory. + // + // This is particularly important for inaccessible mount points such as + // FUSE/sshfs mounts owned by another user. If an error is returned here + // then a single inaccessible entry causes the entire containing directory + // to fail and be omitted from the snapshot, which results in omitting + // other accessible entries in the same directory. + e := filesystemEntry{ + name: TrimShallowSuffix(n), + size: 0, + mtimeNanos: 0, + mode: dirEntry.Type(), + owner: fs.OwnerInfo{}, + device: fs.DeviceInfo{}, + prefix: prefix, } + return newFilesystemErrorEntry(e, err), nil + default: return nil, errors.Wrap(err, "error reading directory") } - - return entryFromDirEntry(n, fi, prefix, options), nil } // NewEntry returns fs.Entry for the specified path, the result will be one of supported entry types: fs.File, fs.Directory, fs.Symlink // or fs.UnsupportedEntry. -// It uses DefaultOptions for configuration. func NewEntry(path string) (fs.Entry, error) { - return NewEntryWithOptions(path, DefaultOptions) -} - -// NewEntryWithOptions returns fs.Entry for the specified path, the result will be one of supported entry types: fs.File, fs.Directory, fs.Symlink -// or fs.UnsupportedEntry. -// It uses the provided Options for configuration. -func NewEntryWithOptions(path string, options *Options) (fs.Entry, error) { path = filepath.Clean(path) fi, err := os.Lstat(path) @@ -143,42 +150,42 @@ func NewEntryWithOptions(path string, options *Options) (fs.Entry, error) { } if path == "/" { - return entryFromDirEntry("/", fi, "", options), nil + return entryFromDirEntry("/", fi, ""), nil } basename, prefix := splitDirPrefix(path) - return entryFromDirEntry(basename, fi, prefix, options), nil + return entryFromDirEntry(basename, fi, prefix), nil } -func entryFromDirEntry(basename string, fi os.FileInfo, prefix string, options *Options) fs.Entry { +func entryFromDirEntry(basename string, fi os.FileInfo, prefix string) fs.Entry { isplaceholder := strings.HasSuffix(basename, ShallowEntrySuffix) maskedmode := fi.Mode() & os.ModeType switch { case maskedmode == os.ModeDir && !isplaceholder: - return newFilesystemDirectory(newEntry(basename, fi, prefix, options)) + return newFilesystemDirectory(newEntry(basename, fi, prefix)) case maskedmode == os.ModeDir && isplaceholder: - return newShallowFilesystemDirectory(newEntry(basename, fi, prefix, options)) + return newShallowFilesystemDirectory(newEntry(basename, fi, prefix)) case maskedmode == os.ModeSymlink && !isplaceholder: - return newFilesystemSymlink(newEntry(basename, fi, prefix, options)) + return newFilesystemSymlink(newEntry(basename, fi, prefix)) case maskedmode == 0 && !isplaceholder: - return newFilesystemFile(newEntry(basename, fi, prefix, options)) + return newFilesystemFile(newEntry(basename, fi, prefix)) case maskedmode == 0 && isplaceholder: - return newShallowFilesystemFile(newEntry(basename, fi, prefix, options)) + return newShallowFilesystemFile(newEntry(basename, fi, prefix)) default: - return newFilesystemErrorEntry(newEntry(basename, fi, prefix, options), fs.ErrUnknown) + return newFilesystemErrorEntry(newEntry(basename, fi, prefix), fs.ErrUnknown) } } var _ os.FileInfo = (*filesystemEntry)(nil) -func newEntry(basename string, fi os.FileInfo, prefix string, options *Options) filesystemEntry { +func newEntry(basename string, fi os.FileInfo, prefix string) filesystemEntry { return filesystemEntry{ TrimShallowSuffix(basename), fi.Size(), @@ -187,6 +194,5 @@ func newEntry(basename string, fi os.FileInfo, prefix string, options *Options) platformSpecificOwnerInfo(fi), platformSpecificDeviceInfo(fi), prefix, - options, } } diff --git a/fs/localfs/local_fs_test.go b/fs/localfs/local_fs_test.go index f44c340d57c..4f412753ac0 100644 --- a/fs/localfs/local_fs_test.go +++ b/fs/localfs/local_fs_test.go @@ -307,276 +307,45 @@ func TestSplitDirPrefix(t *testing.T) { } } -// getOptionsFromEntry extracts the options pointer from an fs.Entry by type assertion. -// Returns nil if the entry doesn't have options or if type assertion fails. -func getOptionsFromEntry(entry fs.Entry) *Options { - switch e := entry.(type) { - case *filesystemDirectory: - return e.options - case *filesystemFile: - return e.options - case *filesystemSymlink: - return e.options - case *filesystemErrorEntry: - return e.options - default: - return nil - } -} - -func TestOptionsPassedToChildEntries(t *testing.T) { - ctx := testlogging.Context(t) - tmp := testutil.TempDirectory(t) - - // Create a test directory structure - require.NoError(t, os.WriteFile(filepath.Join(tmp, "file1.txt"), []byte{1, 2, 3}, 0o777)) - require.NoError(t, os.WriteFile(filepath.Join(tmp, "file2.txt"), []byte{4, 5, 6}, 0o777)) - subdir := filepath.Join(tmp, "subdir") - require.NoError(t, os.Mkdir(subdir, 0o777)) - require.NoError(t, os.WriteFile(filepath.Join(subdir, "subfile.txt"), []byte{7, 8, 9}, 0o777)) - - // Create custom options - customOptions := &Options{ - IgnoreUnreadableDirEntries: true, - } - - // Create directory with custom options - dir, err := DirectoryWithOptions(tmp, customOptions) - require.NoError(t, err) - - // Verify the directory itself has the correct options - dirOptions := getOptionsFromEntry(dir) - require.NotNil(t, dirOptions, "directory should have options") - require.Equal(t, customOptions, dirOptions, "directory should have the same options pointer") - require.True(t, dirOptions.IgnoreUnreadableDirEntries, "directory options should match") - - // Test that options are passed to children via Child() - childFile, err := dir.Child(ctx, "file1.txt") - require.NoError(t, err) - - childOptions := getOptionsFromEntry(childFile) - require.NotNil(t, childOptions, "child file should have options") - require.Equal(t, customOptions, childOptions, "child file should have the same options pointer") - - // Test that options are passed to subdirectories - childDir, err := dir.Child(ctx, "subdir") - require.NoError(t, err) - - subdirOptions := getOptionsFromEntry(childDir) - require.NotNil(t, subdirOptions, "subdirectory should have options") - require.Equal(t, customOptions, subdirOptions, "subdirectory should have the same options pointer") - - // Test that options are passed to nested children - subdirEntry, ok := childDir.(fs.Directory) - require.True(t, ok, "child directory should be a directory") - - nestedFile, err := subdirEntry.Child(ctx, "subfile.txt") - require.NoError(t, err) - - nestedOptions := getOptionsFromEntry(nestedFile) - require.NotNil(t, nestedOptions, "nested file should have options") - require.Equal(t, customOptions, nestedOptions, "nested file should have the same options pointer") -} - -func TestOptionsPassedThroughIteration(t *testing.T) { - ctx := testlogging.Context(t) - tmp := testutil.TempDirectory(t) - - // Create a test directory structure - require.NoError(t, os.WriteFile(filepath.Join(tmp, "file1.txt"), []byte{1, 2, 3}, 0o777)) - require.NoError(t, os.WriteFile(filepath.Join(tmp, "file2.txt"), []byte{4, 5, 6}, 0o777)) - require.NoError(t, os.Mkdir(filepath.Join(tmp, "subdir"), 0o777)) - - // Create custom options - customOptions := &Options{ - IgnoreUnreadableDirEntries: true, - } - - // Create directory with custom options - dir, err := DirectoryWithOptions(tmp, customOptions) - require.NoError(t, err) - - // Iterate through entries and verify all have the same options pointer - iter, err := dir.Iterate(ctx) - require.NoError(t, err) - - defer iter.Close() - - entryCount := 0 - for { - entry, err := iter.Next(ctx) - if err != nil { - t.Fatalf("iteration error: %v", err) - } - - if entry == nil { - break - } - - entryCount++ - entryOptions := getOptionsFromEntry(entry) - require.NotNil(t, entryOptions, "entry %s should have options", entry.Name()) - require.Equal(t, customOptions, entryOptions, "entry %s should have the same options pointer", entry.Name()) +func TestIteratePermissionDenied(t *testing.T) { + if isWindows { + t.Skip("test not applicable on Windows") } - require.Equal(t, 3, entryCount, "should have found 3 entries") -} - -func TestOptionsPassedThroughSymlinkResolution(t *testing.T) { - ctx := testlogging.Context(t) - tmp := testutil.TempDirectory(t) - - // Create a target file - targetFile := filepath.Join(tmp, "target.txt") - require.NoError(t, os.WriteFile(targetFile, []byte{1, 2, 3}, 0o777)) - - // Create a symlink - symlinkPath := filepath.Join(tmp, "link") - require.NoError(t, os.Symlink(targetFile, symlinkPath)) - - // Create custom options - customOptions := &Options{ - IgnoreUnreadableDirEntries: true, + if os.Getuid() == 0 { + t.Skip("test cannot run as root") } - // Create symlink entry with custom options - symlinkEntry, err := NewEntryWithOptions(symlinkPath, customOptions) - require.NoError(t, err) - - // Verify the symlink has the correct options - symlinkOptions := getOptionsFromEntry(symlinkEntry) - require.NotNil(t, symlinkOptions, "symlink should have options") - require.Equal(t, customOptions, symlinkOptions, "symlink should have the same options pointer") - - // Resolve the symlink and verify the resolved entry has the same options - symlink, ok := symlinkEntry.(fs.Symlink) - require.True(t, ok, "entry should be a symlink") - - resolved, err := symlink.Resolve(ctx) - require.NoError(t, err) - - resolvedOptions := getOptionsFromEntry(resolved) - require.NotNil(t, resolvedOptions, "resolved entry should have options") - require.Equal(t, customOptions, resolvedOptions, "resolved entry should have the same options pointer") -} - -func TestOptionsPassedToNewEntry(t *testing.T) { tmp := testutil.TempDirectory(t) - // Create a file - filePath := filepath.Join(tmp, "testfile.txt") - require.NoError(t, os.WriteFile(filePath, []byte{1, 2, 3}, 0o777)) + // Create a directory with files, then remove execute permission. + // Without execute permission, the directory can be listed (read) + // but lstat on children will fail with permission denied. + require.NoError(t, os.WriteFile(filepath.Join(tmp, "a"), []byte{1}, 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "b"), []byte{2}, 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "c"), []byte{3}, 0o644)) - // Create custom options - customOptions := &Options{ - IgnoreUnreadableDirEntries: true, - } + require.NoError(t, os.Chmod(tmp, 0o644)) + t.Cleanup(func() { os.Chmod(tmp, 0o755) }) - // Create entry with custom options - entry, err := NewEntryWithOptions(filePath, customOptions) + dir, err := Directory(tmp) require.NoError(t, err) - // Verify the entry has the correct options - entryOptions := getOptionsFromEntry(entry) - require.NotNil(t, entryOptions, "entry should have options") - require.Equal(t, customOptions, entryOptions, "entry should have the same options pointer") -} - -func TestOptionsPassedToNestedDirectories(t *testing.T) { ctx := testlogging.Context(t) - tmp := testutil.TempDirectory(t) - // Create nested directory structure - level1 := filepath.Join(tmp, "level1") - level2 := filepath.Join(level1, "level2") - level3 := filepath.Join(level2, "level3") - - require.NoError(t, os.MkdirAll(level3, 0o777)) - require.NoError(t, os.WriteFile(filepath.Join(level3, "file.txt"), []byte{1, 2, 3}, 0o777)) - - // Create custom options - customOptions := &Options{ - IgnoreUnreadableDirEntries: true, - } - - // Create root directory with custom options - rootDir, err := DirectoryWithOptions(tmp, customOptions) - require.NoError(t, err) - - // Navigate through nested directories and verify options are passed - currentDir := rootDir - levels := []string{"level1", "level2", "level3"} - - for _, level := range levels { - child, err := currentDir.Child(ctx, level) - require.NoError(t, err) + var entries []fs.Entry - childOptions := getOptionsFromEntry(child) - require.NotNil(t, childOptions, "directory %s should have options", level) - require.Equal(t, customOptions, childOptions, "directory %s should have the same options pointer", level) + err = fs.IterateEntries(ctx, dir, func(ctx context.Context, e fs.Entry) error { + entries = append(entries, e) + return nil + }) - var ok bool + require.NoError(t, err, "iteration should complete without error") + require.Len(t, entries, 3, "should have 3 entries") - currentDir, ok = child.(fs.Directory) - require.True(t, ok, "child should be a directory") + for _, e := range entries { + ee, ok := e.(fs.ErrorEntry) + require.True(t, ok, "entry should be ErrorEntry") + require.True(t, os.IsPermission(ee.ErrorInfo()), "error should be permission denied") } - - // Verify the file in the deepest directory has the same options - file, err := currentDir.Child(ctx, "file.txt") - require.NoError(t, err) - - fileOptions := getOptionsFromEntry(file) - require.NotNil(t, fileOptions, "file should have options") - require.Equal(t, customOptions, fileOptions, "file should have the same options pointer") -} - -func TestDefaultOptionsUsedByDefault(t *testing.T) { - tmp := testutil.TempDirectory(t) - - // Create a file - filePath := filepath.Join(tmp, "testfile.txt") - require.NoError(t, os.WriteFile(filePath, []byte{1, 2, 3}, 0o777)) - - // Use default NewEntry (should use DefaultOptions) - entry, err := NewEntry(filePath) - require.NoError(t, err) - - // Verify the entry has DefaultOptions - entryOptions := getOptionsFromEntry(entry) - require.NotNil(t, entryOptions, "entry should have options") - require.Equal(t, DefaultOptions, entryOptions, "entry should have DefaultOptions pointer") -} - -func TestDifferentOptionsInstances(t *testing.T) { - tmp := testutil.TempDirectory(t) - - // Create two different files - filePath1 := filepath.Join(tmp, "testfile1.txt") - filePath2 := filepath.Join(tmp, "testfile2.txt") - - require.NoError(t, os.WriteFile(filePath1, []byte{1, 2, 3}, 0o777)) - require.NoError(t, os.WriteFile(filePath2, []byte{4, 5, 6}, 0o777)) - - // Create two different options instances with same values - options1 := &Options{IgnoreUnreadableDirEntries: true} - options2 := &Options{IgnoreUnreadableDirEntries: false} - - // Create entries with different options instances - entry1, err := NewEntryWithOptions(filePath1, options1) - require.NoError(t, err) - - entry2, err := NewEntryWithOptions(filePath2, options2) - require.NoError(t, err) - - // Verify they have the correct options pointers - entry1Options := getOptionsFromEntry(entry1) - entry2Options := getOptionsFromEntry(entry2) - - require.NotNil(t, entry1Options) - require.NotNil(t, entry2Options) - require.Equal(t, options1, entry1Options, "entry1 should have options1 pointer") - require.Equal(t, options2, entry2Options, "entry2 should have options2 pointer") - require.NotEqual(t, entry1Options, entry2Options, "entries should have different options pointers") - require.True(t, entry1Options.IgnoreUnreadableDirEntries, "entry1 options should have IgnoreUnreadableDirEntries=true") - require.False(t, entry2Options.IgnoreUnreadableDirEntries, "entry2 options should have IgnoreUnreadableDirEntries=false") } diff --git a/fs/localfs/local_fs_windows.go b/fs/localfs/local_fs_windows.go index 3553075dae7..6a20b813f42 100644 --- a/fs/localfs/local_fs_windows.go +++ b/fs/localfs/local_fs_windows.go @@ -8,7 +8,7 @@ import ( "github.com/kopia/kopia/fs" ) -var isWindows = runtime.GOOS == "windows" +const isWindows = runtime.GOOS == "windows" func platformSpecificOwnerInfo(_ os.FileInfo) fs.OwnerInfo { return fs.OwnerInfo{} @@ -25,7 +25,6 @@ func trailingSeparator(fsd *filesystemDirectory) string { fsd.prefix == `\\?\GLOBALROOT\Device\` && strings.HasPrefix(fsd.Name(), "HarddiskVolumeShadowCopy") && !strings.HasSuffix(fsd.Name(), separatorStr) { - return separatorStr } diff --git a/fs/localfs/shallow_fs.go b/fs/localfs/shallow_fs.go index ae1923d68b2..1dedc4c3af7 100644 --- a/fs/localfs/shallow_fs.go +++ b/fs/localfs/shallow_fs.go @@ -11,6 +11,7 @@ import ( "github.com/kopia/kopia/fs" "github.com/kopia/kopia/internal/atomicfile" + "github.com/kopia/kopia/internal/ospath" "github.com/kopia/kopia/snapshot" ) @@ -26,7 +27,7 @@ func placeholderPath(path string, et snapshot.EntryType) (string, error) { return path + ShallowEntrySuffix, nil case snapshot.EntryTypeDirectory: // Directories and regular files dirpath := path + ShallowEntrySuffix - if err := os.MkdirAll(atomicfile.MaybePrefixLongFilenameOnWindows(dirpath), os.FileMode(dirMode)); err != nil { + if err := os.MkdirAll(ospath.SafeLongFilename(dirpath), os.FileMode(dirMode)); err != nil { return "", errors.Wrap(err, "placeholderPath dir creation") } @@ -64,7 +65,7 @@ func WriteShallowPlaceholder(path string, de *snapshot.DirEntry) (string, error) } func dirEntryFromPlaceholder(path string) (*snapshot.DirEntry, error) { - b, err := os.ReadFile(atomicfile.MaybePrefixLongFilenameOnWindows(path)) + b, err := os.ReadFile(ospath.SafeLongFilename(path)) if err != nil { return nil, errors.Wrap(err, "dirEntryFromPlaceholder reading placeholder") } @@ -89,7 +90,7 @@ type shallowFilesystemDirectory struct { } func checkedDirEntryFromPlaceholder(path, php string) (*snapshot.DirEntry, error) { - if _, err := os.Lstat(atomicfile.MaybePrefixLongFilenameOnWindows(path)); err == nil { + if _, err := os.Lstat(ospath.SafeLongFilename(path)); err == nil { return nil, errors.Errorf("%q, %q exist: shallowrestore tree is corrupt probably because a previous restore into a shallow tree was interrupted", path, php) } diff --git a/go.mod b/go.mod index d8c0bc3fd90..bef3e8044de 100644 --- a/go.mod +++ b/go.mod @@ -2,107 +2,100 @@ module github.com/kopia/kopia go 1.25.0 -toolchain go1.25.8 - require ( - cloud.google.com/go/storage v1.57.2 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + cloud.google.com/go/storage v1.62.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b github.com/chmduquesne/rollinghash v4.0.0+incompatible github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 github.com/chromedp/chromedp v0.14.2 - github.com/coreos/go-systemd/v22 v22.6.0 - github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 + github.com/coreos/go-systemd/v22 v22.7.0 + github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b github.com/edsrzf/mmap-go v1.2.0 - github.com/fatih/color v1.18.0 + github.com/fatih/color v1.19.0 github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c github.com/gofrs/flock v0.13.0 - github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/fswalker v0.3.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/hanwen/go-fuse/v2 v2.9.0 + github.com/hanwen/go-fuse/v2 v2.10.1 github.com/hashicorp/cronexpr v1.1.3 - github.com/klauspost/compress v1.18.2 + github.com/klauspost/compress v1.18.6 github.com/klauspost/pgzip v1.2.6 - github.com/klauspost/reedsolomon v1.12.6 - github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d + github.com/klauspost/reedsolomon v1.14.0 + github.com/kopia/htmluibuild v0.0.1-0.20260502040510-a4505d4145ae github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-colorable v0.1.14 - github.com/mattn/go-isatty v0.0.20 - github.com/minio/minio-go/v7 v7.0.97 - github.com/mocktools/go-smtp-mock/v2 v2.5.1 - github.com/mxk/go-vss v1.2.0 + github.com/minio/minio-go/v7 v7.1.0 + github.com/mocktools/go-smtp-mock/v2 v2.5.4 + github.com/mxk/go-vss v1.2.1 github.com/natefinch/atomic v1.0.1 github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 - github.com/pierrec/lz4 v2.6.1+incompatible github.com/pkg/errors v0.9.1 - github.com/pkg/profile v1.7.0 github.com/pkg/sftp v1.13.10 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.4 + github.com/prometheus/common v0.67.5 github.com/sanity-io/litter v1.5.8 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.11.1 - github.com/studio-b12/gowebdav v0.11.0 + github.com/studio-b12/gowebdav v0.12.0 github.com/tg123/go-htpasswd v1.2.4 - github.com/zalando/go-keyring v0.2.6 + github.com/zalando/go-keyring v0.2.8 github.com/zeebo/blake3 v0.2.4 - go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/zap v1.27.1 - golang.org/x/crypto v0.49.0 + golang.org/x/crypto v0.50.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 - golang.org/x/mod v0.33.0 - golang.org/x/net v0.52.0 - golang.org/x/oauth2 v0.35.0 + golang.org/x/mod v0.34.0 + golang.org/x/net v0.53.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/term v0.41.0 - golang.org/x/text v0.35.0 - google.golang.org/api v0.256.0 - google.golang.org/grpc v1.79.3 + golang.org/x/sys v0.43.0 + golang.org/x/term v0.42.0 + golang.org/x/text v0.36.0 + golang.org/x/time v0.15.0 + google.golang.org/api v0.274.0 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 ) require ( - al.essio.dev/pkg/shellescape v1.5.1 // indirect cel.dev/expr v0.25.1 // indirect - cloud.google.com/go v0.121.6 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.19.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.2 // indirect - cloud.google.com/go/monitoring v1.24.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + cloud.google.com/go/iam v1.7.0 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect - github.com/danieljoos/wincred v1.2.2 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect - github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/frankban/quicktest v1.13.1 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -110,19 +103,18 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/glog v1.2.5 // indirect - github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/kr/fs v0.1.0 // indirect - github.com/minio/crc64nvme v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -133,21 +125,22 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/tinylib/msgp v1.3.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 21d49bc39a1..b41d9092709 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,37 @@ -al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ= +cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= -cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= -cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4= -cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= +cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= @@ -41,14 +39,14 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQ github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= @@ -67,16 +65,12 @@ github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZ github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= -github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -84,8 +78,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= -github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= +github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b h1:qZ21OofI7zneC9dOEqul4FmIWz/YjJJMrf6fL7jrFYQ= +github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= @@ -96,20 +90,16 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= -github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8= -github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -125,53 +115,44 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/fswalker v0.3.3 h1:K2+d6cb3vNFjquVPRObIY+QaXJ6cbleVV6yZWLzkkQ8= github.com/google/fswalker v0.3.3/go.mod h1:9upMSscEE8oRi0WJ0rXZZYya1DmgUtJFhXAw7KNS3c4= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= -github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCgr6qTIzmRp8y9QwDIbqy/nYr9WDPos= github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= -github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +github.com/hanwen/go-fuse/v2 v2.10.1 h1:QAqZuc9+aBtTou+OPruU/hkYQYCkgPtQd2QaepHkTTs= +github.com/hanwen/go-fuse/v2 v2.10.1/go.mod h1:aU7NkGYZUmuJrZapoI3mEcNve7PZTySUOLBuch/vR6U= github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -179,18 +160,14 @@ github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/reedsolomon v1.12.6 h1:8pqE9aECQG/ZFitiUD1xK/E83zwosBAZtE3UbuZM8TQ= -github.com/klauspost/reedsolomon v1.12.6/go.mod h1:ggJT9lc71Vu+cSOPBlxGvBN6TfAS77qB4fp8vJ05NSA= -github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d h1:U3VB/cDMsPW4zB4JRFbVRDzIpPytt889rJUKAG40NPA= -github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto= +github.com/klauspost/reedsolomon v1.14.0 h1:5YSZeclzSYg5nl349+GDG/agDtQ6MZiwUYXvVKN1Jx0= +github.com/klauspost/reedsolomon v1.14.0/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ= +github.com/kopia/htmluibuild v0.0.1-0.20260502040510-a4505d4145ae h1:igSzPZDDs3icBsXWC/2zRFBRlzelXcBSODpxpORf6s8= +github.com/kopia/htmluibuild v0.0.1-0.20260502040510-a4505d4145ae/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -199,22 +176,22 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= -github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= -github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA= -github.com/mocktools/go-smtp-mock/v2 v2.5.1/go.mod h1:Rr8M2njlxx//l5INl2+uESnsL2lDsL24teEykCrGfmE= +github.com/mocktools/go-smtp-mock/v2 v2.5.4 h1:U89Y4SuOhDFUfboMYUtXzWDp7hNLrofRa5yNqGSESSM= +github.com/mocktools/go-smtp-mock/v2 v2.5.4/go.mod h1:qBGjYXy5jKKVFhDnB39DYQfn4hWfcqWAlJTcvrku3rg= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-vss v1.2.0 h1:JpdOPc/P6B3XyRoddn0iMiG/ADBi3AuEsv8RlTb+JeE= -github.com/mxk/go-vss v1.2.0/go.mod h1:ZQ4yFxCG54vqPnCd+p2IxAe5jwZdz56wSjbwzBXiFd8= +github.com/mxk/go-vss v1.2.1 h1:shspH0qgqZ9l5sfIRsXS5BgZXz25/BY+ZQsW0HlD0fM= +github.com/mxk/go-vss v1.2.1/go.mod h1:ZQ4yFxCG54vqPnCd+p2IxAe5jwZdz56wSjbwzBXiFd8= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= @@ -223,14 +200,10 @@ github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+ github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -245,11 +218,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -270,49 +242,50 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU= -github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= +github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU= github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0= -github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= -github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= -github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -323,55 +296,53 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA= +google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMvGOD5eeqqRVv6Z9+BYI+bowK97RCr3W+9M= gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/atomicfile/atomicfile.go b/internal/atomicfile/atomicfile.go index 5769e9093db..567b6593594 100644 --- a/internal/atomicfile/atomicfile.go +++ b/internal/atomicfile/atomicfile.go @@ -3,49 +3,14 @@ package atomicfile import ( "io" - "runtime" - "strings" "github.com/natefinch/atomic" "github.com/kopia/kopia/internal/ospath" ) -// Do not prefix files shorter than this, we are intentionally using less than MAX_PATH -// in Windows to allow some suffixes. -const maxPathLength = 240 - -// MaybePrefixLongFilenameOnWindows prefixes the given filename with \\?\ on Windows -// if the filename is longer than 260 characters, which is required to be able to -// use some low-level Windows APIs. -// Because long file names have certain limitations: -// - we must replace forward slashes with backslashes. -// - dummy path element (\.\) must be removed. -// -// Relative paths are always limited to a total of MAX_PATH characters: -// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation -func MaybePrefixLongFilenameOnWindows(fname string) string { - if runtime.GOOS != "windows" || len(fname) < maxPathLength || - fname[:4] == `\\?\` || !ospath.IsAbs(fname) { - return fname - } - - fixed := strings.ReplaceAll(fname, "/", `\`) - - for { - fixed2 := strings.ReplaceAll(fixed, `\.\`, `\`) - if fixed2 == fixed { - break - } - - fixed = fixed2 - } - - return `\\?\` + fixed -} - // Write is a wrapper around atomic.WriteFile that handles long file names on Windows. func Write(filename string, r io.Reader) error { //nolint:wrapcheck - return atomic.WriteFile(MaybePrefixLongFilenameOnWindows(filename), r) + return atomic.WriteFile(ospath.SafeLongFilename(filename), r) } diff --git a/internal/bigmap/bigmapbench/main.go b/internal/bigmap/bigmapbench/main.go index 760ada05f01..aa99ffeda8a 100644 --- a/internal/bigmap/bigmapbench/main.go +++ b/internal/bigmap/bigmapbench/main.go @@ -7,13 +7,14 @@ import ( "encoding/binary" "flag" "fmt" + "log" "os" + "path/filepath" "runtime" + "runtime/pprof" "sync" "time" - "github.com/pkg/profile" - "github.com/kopia/kopia/internal/bigmap" "github.com/kopia/kopia/internal/clock" "github.com/kopia/kopia/repo/logging" @@ -27,10 +28,11 @@ const ( //nolint:gochecknoglobals var ( - impl = flag.Int("impl", implMapWithEmptyValue, "Select implementation") - profileDir = flag.String("profile-dir", "", "Profile directory") - profileCPU = flag.Bool("profile-cpu", false, "Profile CPU") - profileMemory = flag.Bool("profile-memory", false, "Profile RAM") + impl = flag.Int("impl", implMapWithEmptyValue, "Select implementation") + profileDir = flag.String("profile-dir", "", "Profile directory") + profileCPU = flag.Bool("profile-cpu", false, "Profile CPU") + profileMemory = flag.Bool("profile-memory", false, "Profile memory usage") + profileMemoryRate = flag.Int("profile-memory-rate", -1, "Profile memory rate") ) func main() { @@ -46,17 +48,7 @@ func main() { ms0 runtime.MemStats ) - if *profileDir != "" { - pp := profile.ProfilePath(*profileDir) - - if *profileCPU { - defer profile.Start(pp, profile.CPUProfile).Stop() - } - - if *profileMemory { - defer profile.Start(pp, profile.MemProfile).Stop() - } - } + defer maybeStartProfiling().stop() switch *impl { case implSyncMap: @@ -80,22 +72,6 @@ func main() { t0 := clock.Now() for i := range 300_000_000 { - if i%1_000_000 == 0 && i > 0 { - var ms runtime.MemStats - - runtime.ReadMemStats(&ms) - - alloc := ms.HeapAlloc - ms0.HeapAlloc - dur := clock.Now().Sub(t0).Truncate(time.Second) - - fmt.Printf("elapsed %v count: %v M bytes: %v MB bytes/item: %v Mitems/sec: %.1f\n", - dur, - float64(i)/1e6, - alloc/1e6, - alloc/uint64(i), - float64(i)/dur.Seconds()/1e6) - } - // generate key=sha256(i) without allocations. h.Reset() binary.LittleEndian.PutUint64(num[:], uint64(i)) //nolint:gosec @@ -110,5 +86,121 @@ func main() { case implMapWithValues: bm.PutIfAbsent(ctx, keyBuf[:], keyBuf[:]) } + + count := uint64(i + 1) //nolint:gosec + + if count%1_000_000 == 0 { + var ms runtime.MemStats + + runtime.ReadMemStats(&ms) + + alloc := ms.HeapAlloc - ms0.HeapAlloc + dur := clock.Now().Sub(t0) + + fmt.Printf("elapsed %v, count: %v M, bytes: %v MB, bytes/item: %v, Mitems/sec: %.1f\n", + dur.Truncate(time.Second), + float64(count)/1e6, + alloc/1e6, + alloc/count, + float64(count)/dur.Seconds()/1e6) + } + } +} + +// dirMode is the directory mode for output directories. +const dirMode = 0o700 + +type stopperFn func() + +func (f stopperFn) stop() { + f() +} + +func maybeStartProfiling() stopperFn { + if *profileDir == "" { + return func() {} + } + + // ensure upfront that the pprof output dir can be created. + if err := os.MkdirAll(*profileDir, dirMode); err != nil { + log.Fatalln("could not create directory for profile output:", err) + } + + var cpuProfileStopper stopperFn + + if *profileCPU { + cpuProfileStopper = startCPUProfiling(*profileDir) + } + + if *profileMemory && *profileMemoryRate >= 0 { + runtime.MemProfileRate = *profileMemoryRate + } + + return func() { + if cpuProfileStopper != nil { + cpuProfileStopper() + } + + if *profileMemory { + dumpProfiles(*profileDir) + } + } +} + +func startCPUProfiling(profDir string) stopperFn { + // start CPU profile dumper + f, err := os.Create(filepath.Join(profDir, "cpu.pprof")) //nolint:gosec + if err != nil { + log.Fatalln("could not create CPU profile output file:", err) + } + + // CPU profile profStopper + profStopper := func() { + pprof.StopCPUProfile() + + if err := f.Close(); err != nil { + log.Println("error closing CPU profile output file:", err) + } + } + + if err := pprof.StartCPUProfile(f); err != nil { + profStopper() + + log.Fatalln("could not start CPU profile:", err) + } + + return profStopper +} + +func dumpProfiles(profDir string) { + if err := os.MkdirAll(profDir, dirMode); err != nil { + log.Println("could not create directory for profile output:", err) + + return + } + + runtime.GC() // force GC to include stats since last GC + + for _, p := range pprof.Profiles() { + func() { + fname := filepath.Join(profDir, p.Name()+".pprof") + + f, err := os.Create(fname) //nolint:gosec + if err != nil { + log.Printf("unable to create profile output file '%s': %v", fname, err) + + return + } + + defer func() { + if err := f.Close(); err != nil { + log.Printf("unable to close profile output file '%s': %v", fname, err) + } + }() + + if err := p.WriteTo(f, 0); err != nil { + log.Printf("unable to write profile to file '%s': %v", fname, err) + } + }() } } diff --git a/internal/contentlog/contentlog_json_writer_test.go b/internal/contentlog/contentlog_json_writer_test.go index 27f0835444b..458efaa8695 100644 --- a/internal/contentlog/contentlog_json_writer_test.go +++ b/internal/contentlog/contentlog_json_writer_test.go @@ -3,6 +3,7 @@ package contentlog import ( "encoding/json" "os" + "strings" "testing" "time" @@ -741,11 +742,18 @@ func TestJSONWriter_StringEscapingPerformanceWithManyControlChars(t *testing.T) jw.BeginObject() // Create a string with many control characters to test performance - var testString string - for i := range 100 { - testString += string(rune(i % 32)) // Mix of control chars 0x00-0x1F + const testStringLen = 100 + + var sb strings.Builder + + sb.Grow(testStringLen) + + for i := range testStringLen { + sb.WriteRune(rune(i % 32)) // Mix of control chars 0x00-0x1F } + testString := sb.String() + jw.StringField("manyControlChars", testString) jw.EndObject() diff --git a/internal/epoch/epoch_manager.go b/internal/epoch/epoch_manager.go index f6149875413..7e075817d35 100644 --- a/internal/epoch/epoch_manager.go +++ b/internal/epoch/epoch_manager.go @@ -51,7 +51,7 @@ type Parameters struct { // how frequently each client will list blobs to determine the current epoch. EpochRefreshFrequency time.Duration `json:"EpochRefreshFrequency"` - // number of epochs between full checkpoints. + // number of epochs between range compactions. FullCheckpointFrequency int `json:"FullCheckpointFrequency"` // do not delete uncompacted blobs if the corresponding compacted blob age is less than this. @@ -80,11 +80,6 @@ func (p *Parameters) GetEpochRefreshFrequency() time.Duration { return p.EpochRefreshFrequency } -// GetEpochFullCheckpointFrequency returns the number of epochs between full checkpoints. -func (p *Parameters) GetEpochFullCheckpointFrequency() int { - return p.FullCheckpointFrequency -} - // GetEpochCleanupSafetyMargin returns safety margin to prevent uncompacted blobs from being deleted if the corresponding compacted blob age is less than this. func (p *Parameters) GetEpochCleanupSafetyMargin() time.Duration { return p.CleanupSafetyMargin @@ -127,7 +122,7 @@ func (p *Parameters) Validate() error { } if p.FullCheckpointFrequency <= 0 { - return errors.New("invalid epoch checkpoint frequency") + return errors.New("invalid epoch range compaction period") } if p.CleanupSafetyMargin < p.EpochRefreshFrequency*3 { @@ -325,18 +320,18 @@ func (e *Manager) cleanupInternal(ctx context.Context, cs CurrentSnapshot, p *Pa // may have not observed them yet. maxReplacementTime := maxTime.Add(-p.CleanupSafetyMargin) - var deletedEpochMarkersCount, deletedWatermarksCount atomic.Int64 + var deletedEpochMarkersCount, deletedWatermarksCount atomic.Uint64 eg.Go(func() error { - deleted, err := e.cleanupEpochMarkers(ctx, cs) - deletedEpochMarkersCount.Store(int64(deleted)) + deleted, err := e.cleanupEpochMarkers(ctx, cs, p.DeleteParallelism) + deletedEpochMarkersCount.Store(deleted) return err }) eg.Go(func() error { - deleted, err := e.cleanupWatermarks(ctx, cs, p, maxReplacementTime) - deletedWatermarksCount.Store(int64(deleted)) + deleted, err := e.cleanupWatermarks(ctx, cs, maxReplacementTime, p.DeleteParallelism) + deletedWatermarksCount.Store(deleted) return err }) @@ -346,14 +341,14 @@ func (e *Manager) cleanupInternal(ctx context.Context, cs CurrentSnapshot, p *Pa } result := &maintenancestats.CleanupMarkersStats{ - DeletedEpochMarkerBlobCount: int(deletedEpochMarkersCount.Load()), - DeletedWatermarkBlobCount: int(deletedWatermarksCount.Load()), + DeletedEpochMarkerBlobCount: deletedEpochMarkersCount.Load(), + DeletedWatermarkBlobCount: deletedWatermarksCount.Load(), } return result, nil } -func (e *Manager) cleanupEpochMarkers(ctx context.Context, cs CurrentSnapshot) (int, error) { +func (e *Manager) cleanupEpochMarkers(ctx context.Context, cs CurrentSnapshot, deleteParallelism int) (uint64, error) { // delete epoch markers for epoch < current-1 var toDelete []blob.ID @@ -365,15 +360,10 @@ func (e *Manager) cleanupEpochMarkers(ctx context.Context, cs CurrentSnapshot) ( } } - p, err := e.getParameters(ctx) - if err != nil { - return 0, err - } - - return len(toDelete), errors.Wrap(blob.DeleteMultiple(ctx, e.st, toDelete, p.DeleteParallelism), "error deleting index blob marker") + return uint64(len(toDelete)), errors.Wrap(blob.DeleteMultiple(ctx, e.st, toDelete, deleteParallelism), "error deleting index blob marker") } -func (e *Manager) cleanupWatermarks(ctx context.Context, cs CurrentSnapshot, p *Parameters, maxReplacementTime time.Time) (int, error) { +func (e *Manager) cleanupWatermarks(ctx context.Context, cs CurrentSnapshot, maxReplacementTime time.Time, deleteParallelism int) (uint64, error) { var toDelete []blob.ID for _, bm := range cs.DeletionWatermarkBlobs { @@ -391,7 +381,7 @@ func (e *Manager) cleanupWatermarks(ctx context.Context, cs CurrentSnapshot, p * } } - return len(toDelete), errors.Wrap(blob.DeleteMultiple(ctx, e.st, toDelete, p.DeleteParallelism), "error deleting watermark blobs") + return uint64(len(toDelete)), errors.Wrap(blob.DeleteMultiple(ctx, e.st, toDelete, deleteParallelism), "error deleting watermark blobs") } // CleanupSupersededIndexes cleans up the indexes which have been superseded by compacted ones. @@ -431,13 +421,13 @@ func (e *Manager) CleanupSupersededIndexes(ctx context.Context) (*maintenancesta var toDelete []blob.ID - var deletedTotalSize int64 + var deletedTotalSize uint64 for _, bm := range blobs { if epoch, ok := epochNumberFromBlobID(bm.BlobID); ok { if blobSetWrittenEarlyEnough(cs.SingleEpochCompactionSets[epoch], maxReplacementTime) { toDelete = append(toDelete, bm.BlobID) - deletedTotalSize += bm.Length + deletedTotalSize += maintenancestats.ToUint64(bm.Length) } } } @@ -448,7 +438,7 @@ func (e *Manager) CleanupSupersededIndexes(ctx context.Context) (*maintenancesta return &maintenancestats.CleanupSupersededIndexesStats{ MaxReplacementTime: maxReplacementTime, - DeletedBlobCount: len(toDelete), + DeletedBlobCount: uint64(len(toDelete)), DeletedTotalSize: deletedTotalSize, }, nil } @@ -618,8 +608,8 @@ func (e *Manager) MaybeGenerateRangeCheckpoint(ctx context.Context) (*maintenanc } return &maintenancestats.GenerateRangeCheckpointStats{ - RangeMinEpoch: firstNonRangeCompacted, - RangeMaxEpoch: latestSettled, + RangeMinEpoch: maintenancestats.ToUint64(firstNonRangeCompacted), + RangeMaxEpoch: maintenancestats.ToUint64(latestSettled), }, nil } @@ -756,7 +746,7 @@ func (e *Manager) MaybeAdvanceWriteEpoch(ctx context.Context) (*maintenancestats e.mu.Unlock() result := &maintenancestats.AdvanceEpochStats{ - CurrentEpoch: cs.WriteEpoch, + CurrentEpoch: maintenancestats.ToUint64(cs.WriteEpoch), } if shouldAdvance(cs.UncompactedEpochSets[cs.WriteEpoch], p.MinEpochDuration, p.EpochAdvanceOnCountThreshold, p.EpochAdvanceOnTotalSizeBytesThreshold) { @@ -764,7 +754,7 @@ func (e *Manager) MaybeAdvanceWriteEpoch(ctx context.Context) (*maintenancestats return nil, errors.Wrap(err, "error advancing epoch") } - result.CurrentEpoch = cs.WriteEpoch + 1 + result.CurrentEpoch = maintenancestats.ToUint64(cs.WriteEpoch + 1) result.WasAdvanced = true } @@ -1048,15 +1038,15 @@ func (e *Manager) MaybeCompactSingleEpoch(ctx context.Context) (*maintenancestat uncompactedBlobs = ue } - var uncompactedSize int64 + var uncompactedSize uint64 for _, b := range uncompactedBlobs { - uncompactedSize += b.Length + uncompactedSize += maintenancestats.ToUint64(b.Length) } result := &maintenancestats.CompactSingleEpochStats{ - SupersededIndexBlobCount: len(uncompactedBlobs), + SupersededIndexBlobCount: uint64(len(uncompactedBlobs)), SupersededIndexTotalSize: uncompactedSize, - Epoch: uncompacted, + Epoch: maintenancestats.ToUint64(uncompacted), } contentlog.Log1(ctx, e.log, "starting single-epoch compaction for epoch", result) diff --git a/internal/epoch/epoch_manager_test.go b/internal/epoch/epoch_manager_test.go index c1a83aff791..a1bc48bcbc4 100644 --- a/internal/epoch/epoch_manager_test.go +++ b/internal/epoch/epoch_manager_test.go @@ -624,7 +624,7 @@ func TestMaybeAdvanceEpoch_Empty(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, 0, stats.CurrentEpoch) + require.EqualValues(t, 0, stats.CurrentEpoch) require.False(t, stats.WasAdvanced) // check current epoch again @@ -674,7 +674,7 @@ func TestMaybeAdvanceEpoch(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, 1, stats.CurrentEpoch) + require.EqualValues(t, 1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) // force state refresh @@ -947,7 +947,7 @@ func TestMaybeCompactSingleEpoch_CompactionError(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, j+1, stats.CurrentEpoch) + require.EqualValues(t, j+1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) // force state refresh @@ -1000,7 +1000,7 @@ func TestMaybeCompactSingleEpoch(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, j+1, stats.CurrentEpoch) + require.EqualValues(t, j+1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) // force state refresh @@ -1024,8 +1024,8 @@ func TestMaybeCompactSingleEpoch(t *testing.T) { for j := range newestEpochToCompact { stats, err := te.mgr.MaybeCompactSingleEpoch(ctx) require.NoError(t, err) - require.Equal(t, idxCount, stats.SupersededIndexBlobCount) - require.Equal(t, j, stats.Epoch) + require.EqualValues(t, idxCount, stats.SupersededIndexBlobCount) + require.EqualValues(t, j, stats.Epoch) err = te.mgr.Refresh(ctx) // force state refresh require.NoError(t, err) @@ -1127,7 +1127,7 @@ func TestMaybeGenerateRangeCheckpoint_CompactionError(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, j+1, stats.CurrentEpoch) + require.EqualValues(t, j+1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) @@ -1178,7 +1178,7 @@ func TestMaybeGenerateRangeCheckpoint_FromUncompactedEpochs(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, j+1, stats.CurrentEpoch) + require.EqualValues(t, j+1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) @@ -1193,8 +1193,8 @@ func TestMaybeGenerateRangeCheckpoint_FromUncompactedEpochs(t *testing.T) { stats, err := te.mgr.MaybeGenerateRangeCheckpoint(ctx) require.NoError(t, err) - require.Equal(t, 0, stats.RangeMinEpoch) - require.Equal(t, 8, stats.RangeMaxEpoch) + require.EqualValues(t, 0, stats.RangeMinEpoch) + require.EqualValues(t, 8, stats.RangeMaxEpoch) err = te.mgr.Refresh(ctx) require.NoError(t, err) @@ -1233,7 +1233,7 @@ func TestMaybeGenerateRangeCheckpoint_FromCompactedEpochs(t *testing.T) { stats, err := te.mgr.MaybeAdvanceWriteEpoch(ctx) require.NoError(t, err) - require.Equal(t, j+1, stats.CurrentEpoch) + require.EqualValues(t, j+1, stats.CurrentEpoch) require.True(t, stats.WasAdvanced) err = te.mgr.Refresh(ctx) @@ -1250,8 +1250,8 @@ func TestMaybeGenerateRangeCheckpoint_FromCompactedEpochs(t *testing.T) { for j := range newestEpochToCompact { stats, err := te.mgr.MaybeCompactSingleEpoch(ctx) require.NoError(t, err) - require.Equal(t, idxCount, stats.SupersededIndexBlobCount) - require.Equal(t, j, stats.Epoch) + require.EqualValues(t, idxCount, stats.SupersededIndexBlobCount) + require.EqualValues(t, j, stats.Epoch) err = te.mgr.Refresh(ctx) // force state refresh require.NoError(t, err) @@ -1270,8 +1270,8 @@ func TestMaybeGenerateRangeCheckpoint_FromCompactedEpochs(t *testing.T) { stats, err := te.mgr.MaybeGenerateRangeCheckpoint(ctx) require.NoError(t, err) - require.Equal(t, 0, stats.RangeMinEpoch) - require.Equal(t, 8, stats.RangeMaxEpoch) + require.EqualValues(t, 0, stats.RangeMinEpoch) + require.EqualValues(t, 8, stats.RangeMaxEpoch) err = te.mgr.Refresh(ctx) require.NoError(t, err) @@ -1313,7 +1313,7 @@ func TestValidateParameters(t *testing.T) { MinEpochDuration: 1 * time.Hour, EpochRefreshFrequency: 10 * time.Minute, FullCheckpointFrequency: -1, - }, "invalid epoch checkpoint frequency", + }, "invalid epoch range compaction period", }, { Parameters{ diff --git a/internal/insecureserverbind/insecureserverbind.go b/internal/insecureserverbind/insecureserverbind.go new file mode 100644 index 00000000000..f095aeedd98 --- /dev/null +++ b/internal/insecureserverbind/insecureserverbind.go @@ -0,0 +1,129 @@ +// Package insecureserverbind validates listen addresses for insecure, unauthenticated Kopia servers. +package insecureserverbind + +import ( + "errors" + "fmt" + "net" + "net/url" + "strings" +) + +// AllowDangerousUnauthenticatedNetworkFlag is the CLI flag that disables bind restrictions. +const AllowDangerousUnauthenticatedNetworkFlag = "allow-extremely-dangerous-unauthenticated-server-on-the-network" + +// AllowDangerousUnauthenticatedNetworkFlagHelp is the kingpin description for that flag. +const AllowDangerousUnauthenticatedNetworkFlagHelp = "Allow unauthenticated server to listen on non-loopback addresses; " + + "exposes full repository and control API to the network without authentication which allows any external attacker to take full control of the server host (extremely dangerous)" + +// ErrDisallowedPublicBind is returned when the address would expose an unauthenticated server beyond loopback. +var ErrDisallowedPublicBind = errors.New("refusing to expose unauthenticated server on non-loopback network bind") + +// RestrictionApplies reports whether insecure unauthenticated bind checks must run. +func RestrictionApplies(insecure, withoutPassword, allowDangerousNetwork bool) bool { + return insecure && withoutPassword && !allowDangerousNetwork +} + +// ValidateListenAddressIfRestricted runs [ValidateListenAddressFlag] only when [RestrictionApplies] is true. +func ValidateListenAddressIfRestricted(insecure, withoutPassword, allowDangerousNetwork bool, address string) error { + if !RestrictionApplies(insecure, withoutPassword, allowDangerousNetwork) { + return nil + } + + return ValidateListenAddressFlag(address) +} + +// ValidateListenerAddrIfRestricted runs [ValidateListenerAddr] only when [RestrictionApplies] is true. +func ValidateListenerAddrIfRestricted(insecure, withoutPassword, allowDangerousNetwork bool, addr net.Addr) error { + if !RestrictionApplies(insecure, withoutPassword, allowDangerousNetwork) { + return nil + } + + return ValidateListenerAddr(addr) +} + +func stripProtocol(addr string) string { + return strings.TrimPrefix(strings.TrimPrefix(addr, "https://"), "http://") +} + +// ParseListenHost extracts the host part of a server listen address flag value. +// If isUnix is true, host is empty and the address refers to a Unix domain socket. +// +// Unix detection runs after stripping a leading http:// or https:// (same as the server’s +// stripProtocol). Any form that becomes unix:… is treated as a Unix socket, including: +// - unix:/path/to/socket +// - http://unix:/path/to/socket +// - https://unix:/path/to/socket +func ParseListenHost(address string) (host string, isUnix bool, err error) { + stripped := stripProtocol(address) + if strings.HasPrefix(stripped, "unix:") { + return "", true, nil + } + + s := stripped + if !strings.Contains(s, "://") { + s = "http://" + s + } + + u, err := url.Parse(s) + if err != nil { + return "", false, fmt.Errorf("parsing listen address: %w", err) + } + + return u.Hostname(), false, nil +} + +// ValidateListenAddressFlag checks that --address is safe for an insecure server without a UI password. +func ValidateListenAddressFlag(address string) error { + host, isUnix, err := ParseListenHost(address) + if err != nil { + return err + } + + if isUnix { + return nil + } + + if host == "" { + return fmt.Errorf("%w: missing host in listen address %q binds all interfaces; use loopback, a unix socket, or pass --%s (extremely dangerous)", + ErrDisallowedPublicBind, address, AllowDangerousUnauthenticatedNetworkFlag) + } + + if strings.EqualFold(host, "localhost") { + return nil + } + + if ip := net.ParseIP(host); ip != nil { + if ip.IsLoopback() { + return nil + } + + return fmt.Errorf("%w: %q is not a loopback address; pass --%s only in isolated lab environments (extremely dangerous)", + ErrDisallowedPublicBind, host, AllowDangerousUnauthenticatedNetworkFlag) + } + + return fmt.Errorf("%w: hostname %q is not localhost; pass --%s only in isolated lab environments (extremely dangerous)", + ErrDisallowedPublicBind, host, AllowDangerousUnauthenticatedNetworkFlag) +} + +// ValidateListenerAddr checks the bound listener address after Listen (covers socket activation). +func ValidateListenerAddr(addr net.Addr) error { + switch a := addr.(type) { + case *net.UnixAddr: + return nil + case *net.TCPAddr: + if a.IP != nil && a.IP.IsLoopback() { + return nil + } + + return fmt.Errorf("%w: listener %v is not loopback; pass --%s only in isolated lab environments (extremely dangerous)", + ErrDisallowedPublicBind, addr, AllowDangerousUnauthenticatedNetworkFlag) + default: + if addr.Network() == "unix" { + return nil + } + + return fmt.Errorf("%w: cannot validate listener type %T %v; pass --%s only if you accept the risk", + ErrDisallowedPublicBind, addr, addr, AllowDangerousUnauthenticatedNetworkFlag) + } +} diff --git a/internal/insecureserverbind/insecureserverbind_test.go b/internal/insecureserverbind/insecureserverbind_test.go new file mode 100644 index 00000000000..7bb90a759b8 --- /dev/null +++ b/internal/insecureserverbind/insecureserverbind_test.go @@ -0,0 +1,280 @@ +package insecureserverbind + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRestrictionApplies(t *testing.T) { + t.Parallel() + + cases := []struct { + insecure, withoutPassword, allowDangerous bool + want bool + name string + }{ + {true, true, false, true, "all_restriction_flags"}, + {true, true, true, false, "escape_hatch"}, + {true, false, false, false, "no_without_password"}, + {false, true, false, false, "no_insecure"}, + {false, false, false, false, "neither"}, + {false, false, true, false, "neither_plus_escape"}, + {true, false, true, false, "insecure_escape_no_nopass"}, + {false, true, true, false, "nopass_escape_no_insecure"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := RestrictionApplies(tc.insecure, tc.withoutPassword, tc.allowDangerous) + require.Equal(t, tc.want, got) + }) + } +} + +func TestValidateListenAddressIfRestricted(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + insecure, withoutPassword, allowDangerous bool + address string + wantErr bool + }{ + { + name: "when_restriction_does_not_apply_bad_address_ignored", + // not insecure+without-password: bad address is not validated + insecure: false, withoutPassword: true, allowDangerous: false, + address: "http://0.0.0.0:0", wantErr: false, + }, + { + name: "escape_hatch_skips_validation", + insecure: true, withoutPassword: true, allowDangerous: true, + address: "http://0.0.0.0:0", wantErr: false, + }, + { + name: "restricted_non_loopback_rejected", + insecure: true, withoutPassword: true, allowDangerous: false, + address: "http://0.0.0.0:0", wantErr: true, + }, + { + name: "restricted_loopback_ok", + insecure: true, withoutPassword: true, allowDangerous: false, + address: "http://127.0.0.1:0", wantErr: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := ValidateListenAddressIfRestricted( + tc.insecure, tc.withoutPassword, tc.allowDangerous, tc.address) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) + + return + } + + require.NoError(t, err) + }) + } +} + +func TestValidateListenerAddrIfRestricted(t *testing.T) { + t.Parallel() + + pub := &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 80} + loopback := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1} + + cases := []struct { + name string + insecure, withoutPassword, allowDangerous bool + addr net.Addr + wantErr bool + }{ + { + name: "when_restriction_does_not_apply_public_listener_ignored", + insecure: false, withoutPassword: true, allowDangerous: false, + addr: pub, wantErr: false, + }, + { + name: "escape_hatch_skips_validation", + insecure: true, withoutPassword: true, allowDangerous: true, + addr: pub, wantErr: false, + }, + { + name: "restricted_public_listener_rejected", + insecure: true, withoutPassword: true, allowDangerous: false, + addr: pub, wantErr: true, + }, + { + name: "restricted_loopback_ok", + insecure: true, withoutPassword: true, allowDangerous: false, + addr: loopback, wantErr: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := ValidateListenerAddrIfRestricted( + tc.insecure, tc.withoutPassword, tc.allowDangerous, tc.addr) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) + + return + } + + require.NoError(t, err) + }) + } +} + +func TestParseListenHost(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + wantHost string + wantUnix bool + wantError bool + }{ + {"http://127.0.0.1:51515", "127.0.0.1", false, false}, + {"https://127.0.0.1:51515", "127.0.0.1", false, false}, + {"127.0.0.1:51515", "127.0.0.1", false, false}, + {"http://LOCALHOST:0", "LOCALHOST", false, false}, + {"http://[::1]:123", "::1", false, false}, + {"unix:/tmp/kopia.sock", "", true, false}, + {"http://unix:/wrong", "", true, false}, + {"http://:51515", "", false, false}, + {"http://0.0.0.0:0", "0.0.0.0", false, false}, + {"http://192.0.2.1:1", "192.0.2.1", false, false}, + {"http://example.com:80", "example.com", false, false}, + } + + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + t.Parallel() + + host, isUnix, err := ParseListenHost(tc.in) + if tc.wantError { + require.Error(t, err) + + return + } + + require.NoError(t, err) + require.Equal(t, tc.wantHost, host) + require.Equal(t, tc.wantUnix, isUnix) + }) + } +} + +func TestValidateListenAddressFlag(t *testing.T) { + t.Parallel() + + ok := []string{ + "http://127.0.0.1:51515", + "http://localhost:0", + "http://LoCaLhOsT:51515", + "http://LOCALHOST:9999", + "http://[::1]:123", + "http://127.0.0.2:1", + "unix:/tmp/foo.sock", + "https://127.0.0.1:1", + } + + for _, addr := range ok { + t.Run(addr, func(t *testing.T) { + t.Parallel() + require.NoError(t, ValidateListenAddressFlag(addr)) + }) + } + + bad := []string{ + "http://0.0.0.0:0", + "http://:51515", + "http://192.0.2.1:80", + "http://example.com:80", + } + + for _, addr := range bad { + t.Run(addr, func(t *testing.T) { + t.Parallel() + + err := ValidateListenAddressFlag(addr) + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) + require.ErrorContains(t, err, AllowDangerousUnauthenticatedNetworkFlag) + }) + } +} + +func TestValidateListenerAddr(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateListenerAddr(&net.UnixAddr{Name: "/tmp/x", Net: "unix"})) + + require.NoError(t, ValidateListenerAddr(&net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 51515, + })) + + require.NoError(t, ValidateListenerAddr(&net.TCPAddr{ + IP: net.ParseIP("127.0.0.2"), + Port: 1, + })) + + require.NoError(t, ValidateListenerAddr(&net.TCPAddr{ + IP: net.ParseIP("::1"), + Port: 1, + })) + + err := ValidateListenerAddr(&net.TCPAddr{ + IP: net.ParseIP("192.0.2.1"), + Port: 80, + }) + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) + require.ErrorContains(t, err, AllowDangerousUnauthenticatedNetworkFlag) + + err = ValidateListenerAddr(&net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), + Port: 51515, + }) + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) + + err = ValidateListenerAddr(&net.TCPAddr{ + Port: 51515, + }) + require.Error(t, err) +} + +type stubAddr struct { + network, s string +} + +func (a stubAddr) Network() string { return a.network } +func (a stubAddr) String() string { return a.s } + +func TestValidateListenerAddr_unknownType(t *testing.T) { + t.Parallel() + + err := ValidateListenerAddr(stubAddr{network: "tcp", s: "192.0.2.1:80"}) + require.Error(t, err) + require.ErrorIs(t, err, ErrDisallowedPublicBind) +} + +func TestValidateListenerAddr_stubUnixNetwork(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateListenerAddr(stubAddr{network: "unix", s: "/tmp/kopia.sock"})) +} diff --git a/internal/mockfs/mockfs.go b/internal/mockfs/mockfs.go index 1a485201fe2..c23405c4401 100644 --- a/internal/mockfs/mockfs.go +++ b/internal/mockfs/mockfs.go @@ -204,14 +204,28 @@ func (imd *Directory) AddDir(name string, permissions os.FileMode) *Directory { return subdir } -// AddErrorEntry adds a fake directory with a given name and permissions. -func (imd *Directory) AddErrorEntry(name string, permissions os.FileMode, err error) *ErrorEntry { +// AddErrorEntryDir adds a fake directory-typed error entry with a given name and permissions. +func (imd *Directory) AddErrorEntryDir(name string, permissions os.FileMode, err error) *ErrorEntry { + return imd.addErrorEntry(name, permissions|os.ModeDir, err) +} + +// AddErrorEntryFile adds a fake file-typed error entry with a given name and permissions. +func (imd *Directory) AddErrorEntryFile(name string, permissions os.FileMode, err error) *ErrorEntry { + return imd.addErrorEntry(name, permissions&^os.ModeDir, err) +} + +// AddErrorEntryIrregular adds a fake irregular-typed error entry with a given name and permissions. +func (imd *Directory) AddErrorEntryIrregular(name string, permissions os.FileMode, err error) *ErrorEntry { + return imd.addErrorEntry(name, permissions|os.ModeIrregular, err) +} + +func (imd *Directory) addErrorEntry(name string, permissions os.FileMode, err error) *ErrorEntry { imd, name = imd.resolveSubdir(name) ee := &ErrorEntry{ entry: entry{ name: name, - mode: permissions | os.ModeDir, + mode: permissions, modTime: DefaultModTime, }, err: err, diff --git a/internal/mount/mount_net_use.go b/internal/mount/mount_net_use.go index b5987d94b8f..20f8c74f4c4 100644 --- a/internal/mount/mount_net_use.go +++ b/internal/mount/mount_net_use.go @@ -61,7 +61,7 @@ func netUseMount(ctx context.Context, driveLetter, webdavURL string) (string, er // colon. s := bufio.NewScanner(strings.NewReader(out)) for s.Scan() { - for _, word := range strings.Split(s.Text(), " ") { + for word := range strings.SplitSeq(s.Text(), " ") { if isWindowsDrive(word) { return word, nil } diff --git a/internal/ospath/ospath_nonwindows.go b/internal/ospath/ospath_nonwindows.go new file mode 100644 index 00000000000..22a7f03fa09 --- /dev/null +++ b/internal/ospath/ospath_nonwindows.go @@ -0,0 +1,17 @@ +//go:build !windows + +package ospath + +// SafeLongFilename handles long absolute file paths in a platform-specific manner. +// Currently it only handles absolute paths on Windows. It is a no-op on other +// platforms. +// +// On Windows, it prefixes the given filename with \\?\ when the filename length +// approximates MAX_PATH characters, which is required to be able to use some +// low-level Windows APIs. +// +// Relative paths are always limited to a total of MAX_PATH characters: +// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +func SafeLongFilename(fname string) string { + return fname +} diff --git a/internal/ospath/ospath_windows.go b/internal/ospath/ospath_windows.go index 6587775f434..be20d9c372b 100644 --- a/internal/ospath/ospath_windows.go +++ b/internal/ospath/ospath_windows.go @@ -2,9 +2,43 @@ package ospath import ( "os" + "strings" ) func init() { userSettingsDir = os.Getenv("APPDATA") userLogsDir = os.Getenv("LOCALAPPDATA") } + +// SafeLongFilename prefixes the given filename with \\?\ on Windows when the +// filename length approximates MAX_PATH characters, which is required to be +// able to use some low-level Windows APIs. +// Because long file names have certain limitations: +// - we must replace forward slashes with backslashes. +// - dummy path element (\.\) must be removed. +// +// Relative paths are always limited to a total of MAX_PATH characters (typically 260): +// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +func SafeLongFilename(fname string) string { + // Do not prefix when the name is shorter than this. + // Intentionally using less than MAX_PATH in Windows to allow some suffixes. + const maxPathLength = 240 + + if len(fname) < maxPathLength || + fname[:4] == `\\?\` || !IsAbs(fname) { + return fname + } + + fixed := strings.ReplaceAll(fname, "/", `\`) + + for { + fixed2 := strings.ReplaceAll(fixed, `\.\`, `\`) + if fixed2 == fixed { + break + } + + fixed = fixed2 + } + + return `\\?\` + fixed +} diff --git a/internal/atomicfile/atomicfile_test.go b/internal/ospath/ospath_windows_test.go similarity index 85% rename from internal/atomicfile/atomicfile_test.go rename to internal/ospath/ospath_windows_test.go index 30786e147be..9e4611bde2c 100644 --- a/internal/atomicfile/atomicfile_test.go +++ b/internal/ospath/ospath_windows_test.go @@ -1,4 +1,4 @@ -package atomicfile +package ospath import ( "runtime" @@ -6,13 +6,13 @@ import ( "testing" ) -var veryLongSegment = strings.Repeat("f", 270) - -func TestMaybePrefixLongFilenameOnWindows(t *testing.T) { +func TestSafeLongFilename_Windows(t *testing.T) { if runtime.GOOS != "windows" { - return + t.Skip("Windows-only test") } + veryLongSegment := strings.Repeat("f", 270) + cases := []struct { input string want string @@ -37,7 +37,7 @@ func TestMaybePrefixLongFilenameOnWindows(t *testing.T) { } for _, tc := range cases { - if got := MaybePrefixLongFilenameOnWindows(tc.input); got != tc.want { + if got := SafeLongFilename(tc.input); got != tc.want { t.Errorf("invalid result for %v: got %v, want %v", tc.input, got, tc.want) } } diff --git a/internal/retry/retry.go b/internal/retry/retry.go index 6310c601dc3..bd1de0560a6 100644 --- a/internal/retry/retry.go +++ b/internal/retry/retry.go @@ -100,6 +100,13 @@ func WithExponentialBackoffNoValue(ctx context.Context, desc string, attempt fun return err } +// NoValueFn is an adapter from func() error to func() (any, error). +func NoValueFn(f func() error) func() (any, error) { + return func() (any, error) { + return nil, f() + } +} + // Always is a retry function that retries all errors. func Always(error) bool { return true diff --git a/internal/server/server.go b/internal/server/server.go index cb9b75fc5f4..1407448b009 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -15,7 +15,7 @@ import ( "sync" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/pkg/errors" diff --git a/internal/tempfile/tempfile_linux.go b/internal/tempfile/tempfile_linux.go index d0598039383..f70518559f8 100644 --- a/internal/tempfile/tempfile_linux.go +++ b/internal/tempfile/tempfile_linux.go @@ -17,11 +17,11 @@ func CreateAutoDelete() (*os.File, error) { // on reasonably modern Linux (3.11 and above) O_TMPFILE is supported, // which creates invisible, unlinked file in a given directory. fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, permissions) - if err == nil { + if err == nil && fd >= 0 { return os.NewFile(uintptr(fd), ""), nil } - if errors.Is(err, syscall.EISDIR) || errors.Is(err, syscall.EOPNOTSUPP) { + if err == nil || errors.Is(err, syscall.EISDIR) || errors.Is(err, syscall.EOPNOTSUPP) { return createUnixFallback() } diff --git a/internal/testutil/tmpdir.go b/internal/testutil/tmpdir.go index 1d39e1332af..0ab7919749d 100644 --- a/internal/testutil/tmpdir.go +++ b/internal/testutil/tmpdir.go @@ -5,6 +5,7 @@ import ( "math/rand" "os" "path/filepath" + "strconv" "strings" "testing" "time" @@ -90,6 +91,12 @@ func TempDirectoryShort(tb testing.TB) string { return d } +func getEnvVarBool(name string) bool { + s, err := strconv.ParseBool(os.Getenv(name)) + + return err == nil && s +} + // TempLogDirectory returns a temporary directory used for storing logs. // If KOPIA_LOGS_DIR is provided. func TempLogDirectory(tb testing.TB) string { @@ -109,12 +116,12 @@ func TempLogDirectory(tb testing.TB) string { require.NoError(tb, os.MkdirAll(logsDir, logsDirPermissions)) tb.Cleanup(func() { - if os.Getenv("KOPIA_KEEP_LOGS") != "" { + if getEnvVarBool("KOPIA_KEEP_LOGS") { tb.Logf("logs preserved in %v", logsDir) return } - if tb.Failed() && os.Getenv("KOPIA_DISABLE_LOG_DUMP_ON_FAILURE") == "" { + if tb.Failed() && !getEnvVarBool("KOPIA_DISABLE_LOG_DUMP_ON_FAILURE") { dumpLogs(tb, logsDir) } diff --git a/repo/blob/azure/azure_storage.go b/repo/blob/azure/azure_storage.go index e82fef3656a..8092cac720b 100644 --- a/repo/blob/azure/azure_storage.go +++ b/repo/blob/azure/azure_storage.go @@ -477,7 +477,7 @@ func getAZService(opt *Options, storageHostname string) (*azblob.Client, error) service, serviceErr = azblob.NewClient(fmt.Sprintf("%s://%s/", protocol, storageHostname), cred, clientOptions) default: - return nil, errors.New("one of the storage key, SAS token, client secret, client certificate, or Azure Federated Token must be provided") + return nil, errors.New("one of the storage key, SAS token, client secret, client certificate, or Azure Federated Token file must be provided") } return service, errors.Wrap(serviceErr, "unable to create azure client") diff --git a/repo/blob/b2/b2_storage.go b/repo/blob/b2/b2_storage.go index 595f3dfacbe..a445e255b31 100644 --- a/repo/blob/b2/b2_storage.go +++ b/repo/blob/b2/b2_storage.go @@ -16,6 +16,7 @@ import ( "github.com/kopia/kopia/internal/timestampmeta" "github.com/kopia/kopia/repo/blob" "github.com/kopia/kopia/repo/blob/retrying" + "github.com/kopia/kopia/repo/logging" ) const ( @@ -24,6 +25,8 @@ const ( timeMapKey = "kopia-mtime" // case is important, must be all-lowercase ) +var log = logging.Module("b2") + type b2Storage struct { Options blob.DefaultProviderImplementation @@ -247,7 +250,9 @@ func (s *b2Storage) String() string { } // New creates new B2-backed storage with specified options. -func New(_ context.Context, opt *Options, isCreate bool) (blob.Storage, error) { +func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) { + log(ctx).Warn("The B2 storage provider is deprecated and will be removed in the future, use the S3-compatible storage provider instead") + _ = isCreate if opt.BucketName == "" { diff --git a/repo/blob/filesystem/filesystem_storage.go b/repo/blob/filesystem/filesystem_storage.go index 4e9a1e85274..39740987073 100644 --- a/repo/blob/filesystem/filesystem_storage.go +++ b/repo/blob/filesystem/filesystem_storage.go @@ -4,6 +4,7 @@ package filesystem import ( "context" "crypto/rand" + stderrors "errors" "fmt" "io" "os" @@ -154,7 +155,7 @@ func (fs *fsImpl) GetMetadataFromPath(ctx context.Context, dirPath, path string) }, fs.isRetriable) } -//nolint:wrapcheck,gocyclo +//nolint:wrapcheck func (fs *fsImpl) PutBlobInPath(ctx context.Context, dirPath, path string, data blob.Bytes, opts blob.PutOptions) error { _ = dirPath @@ -165,25 +166,12 @@ func (fs *fsImpl) PutBlobInPath(ctx context.Context, dirPath, path string, data return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "do-not-recreate") } - return retry.WithExponentialBackoffNoValue(ctx, "PutBlobInPath:"+path, func() error { - randSuffix := make([]byte, tempFileRandomSuffixLen) - if _, err := rand.Read(randSuffix); err != nil { - return errors.Wrap(err, "can't get random bytes") - } - - tempFile := fmt.Sprintf("%s.tmp.%x", path, randSuffix) + const maxAttempts = 2 - f, err := fs.createTempFileAndDir(tempFile) + _, err := retry.WithExponentialBackoffMaxRetries(ctx, maxAttempts, "PutBlobInPath:"+path, retry.NoValueFn(func() error { + tempFile, err := fs.createTempFileWithData(path, data) if err != nil { - return errors.Wrap(err, "cannot create temporary file") - } - - if _, err = data.WriteTo(f); err != nil { - return errors.Wrap(err, "can't write temporary file") - } - - if err = f.Close(); err != nil { - return errors.Wrap(err, "can't close temporary file") + return err } err = fs.osi.Rename(tempFile, path) @@ -218,7 +206,53 @@ func (fs *fsImpl) PutBlobInPath(ctx context.Context, dirPath, path string, data } return nil - }, fs.isRetriable) + }), fs.isRetriable) + + return err +} + +// createTempFileWithData creates a temporary file, writes data to it, syncs and closes it. +// Returns the name of the temporary file and an error. +// If there is an error writing, syncing, or closing the file, the temporary file is removed. +func (fs *fsImpl) createTempFileWithData(path string, data blob.Bytes) (name string, err error) { + randSuffix := make([]byte, tempFileRandomSuffixLen) + if _, err := rand.Read(randSuffix); err != nil { + return "", errors.Wrap(err, "can't get random bytes for temporary filename") + } + + tempFile := fmt.Sprintf("%s.tmp.%x", path, randSuffix) + + f, err := fs.createTempFileAndDir(tempFile) + if err != nil { + return "", errors.Wrap(err, "cannot create temporary file") + } + + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = stderrors.Join(err, errors.Wrap(closeErr, "can't close temporary file")) + } + + // remove temp file when any of the operations fail + if err != nil { + name = "" + + if removeErr := fs.osi.Remove(tempFile); removeErr != nil { + err = stderrors.Join(err, errors.Wrap(removeErr, "can't remove temp file after error")) + } + } + }() + + if _, err = data.WriteTo(f); err != nil { + return "", errors.Wrap(err, "can't write temporary file") + } + + if err = f.Sync(); err != nil { + return "", errors.Wrap(err, "can't sync temporary file data") + } + + // f closed in deferred cleanup function + + return tempFile, nil } func (fs *fsImpl) createTempFileAndDir(tempFile string) (osWriteFile, error) { diff --git a/repo/blob/filesystem/filesystem_storage_capacity_windows.go b/repo/blob/filesystem/filesystem_storage_capacity_windows.go index 4780befc45a..9446ff7d9f0 100644 --- a/repo/blob/filesystem/filesystem_storage_capacity_windows.go +++ b/repo/blob/filesystem/filesystem_storage_capacity_windows.go @@ -11,7 +11,7 @@ import ( "github.com/kopia/kopia/repo/blob" ) -func (fs *fsStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) { +func (fs *fsStorage) GetCapacity(_ context.Context) (blob.Capacity, error) { var c blob.Capacity pathPtr, err := windows.UTF16PtrFromString(fs.RootPath) diff --git a/repo/blob/filesystem/filesystem_storage_sync_test.go b/repo/blob/filesystem/filesystem_storage_sync_test.go new file mode 100644 index 00000000000..ba907b7330e --- /dev/null +++ b/repo/blob/filesystem/filesystem_storage_sync_test.go @@ -0,0 +1,168 @@ +package filesystem + +import ( + "os" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kopia/kopia/internal/gather" + "github.com/kopia/kopia/internal/testlogging" + "github.com/kopia/kopia/internal/testutil" + "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/blob/sharded" +) + +type verifySyncBeforeCloseFile struct { + osWriteFile // +checklocksignore set on instantiation + + notifyClose func() // +checklocksignore set on instantiation + notifyDirtyClose func() // +checklocksignore set on instantiation + + mu sync.Mutex + // +checklocks:mu + dirty bool +} + +func (vf *verifySyncBeforeCloseFile) Write(p []byte) (n int, err error) { + vf.mu.Lock() + defer vf.mu.Unlock() + + vf.dirty = true + + return vf.osWriteFile.Write(p) +} + +func (vf *verifySyncBeforeCloseFile) Sync() error { + vf.mu.Lock() + defer vf.mu.Unlock() + + err := vf.osWriteFile.Sync() + if err == nil { + vf.dirty = false + } + + return err +} + +func (vf *verifySyncBeforeCloseFile) Close() error { + dirty, err := func() (bool, error) { + vf.mu.Lock() + defer vf.mu.Unlock() + + return vf.dirty, vf.osWriteFile.Close() + }() + + if dirty { + vf.notifyDirtyClose() + } + + vf.notifyClose() + + return err +} + +type mockOSForSyncTest struct { + mockOS + + fileOpenCount atomic.Uint32 + fileCloseCount atomic.Uint32 + dirtyClose atomic.Bool +} + +func (osi *mockOSForSyncTest) Open(fname string) (osReadFile, error) { + f, err := osi.mockOS.Open(fname) + if err != nil { + return nil, err + } + + osi.fileOpenCount.Add(1) + + return f, nil +} + +func (osi *mockOSForSyncTest) CreateNewFile(fname string, perm os.FileMode) (osWriteFile, error) { + wf, err := osi.mockOS.CreateNewFile(fname, perm) + if err != nil { + return nil, err + } + + osi.fileOpenCount.Add(1) + + return &verifySyncBeforeCloseFile{ + osWriteFile: wf, + notifyClose: func() { osi.fileCloseCount.Add(1) }, + notifyDirtyClose: func() { osi.dirtyClose.Store(true) }, + }, nil +} + +// These tests reuse the retry/error-count mock to assert sync handling in PutBlob. +func TestPutBlob_SyncBeforeClose(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + osi := &mockOSForSyncTest{ + mockOS: mockOS{ + osInterface: realOS{}, + }, + } + + st, err := New(ctx, &Options{ + Path: testutil.TempDirectory(t), + Options: sharded.Options{DirectoryShards: []int{1}}, + + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + + t.Cleanup(func() { _ = st.Close(ctx) }) + + err = st.PutBlob(ctx, "blob-sync-ok", gather.FromSlice([]byte("hello")), blob.PutOptions{}) + + require.False(t, osi.dirtyClose.Load(), "close called without calling sync after a write") + require.Equal(t, osi.fileOpenCount.Load(), osi.fileCloseCount.Load(), "calls to file.Close() must match number of opened files()") + require.NoError(t, err) + + var buf gather.WriteBuffer + t.Cleanup(buf.Close) + + err = st.GetBlob(ctx, "blob-sync-ok", 0, -1, &buf) + require.NoError(t, err) + require.Equal(t, []byte("hello"), buf.ToByteSlice()) +} + +func TestPutBlob_FailsOnSyncError(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + dataDir := testutil.TempDirectory(t) + + osi := newMockOS() + + st, err := New(ctx, &Options{ + Path: dataDir, + Options: sharded.Options{DirectoryShards: []int{1}}, + + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + t.Cleanup(func() { _ = st.Close(ctx) }) + + // Test HACK: write a dummy blob to force writing the sharding configuration file, so writing the + // config file does not interfere with the test. While this is coupled to the specifics of the + // current implementation, it is required to be able to test the failure case. + err = st.PutBlob(ctx, "dummy", gather.FromSlice([]byte("hello")), blob.PutOptions{}) + require.NoError(t, err) + + // Inject a failure per create (re-)try, 10 is the default number of retries + osi.writeFileSyncRemainingErrors.Store(10) + + err = st.PutBlob(ctx, "blob-sync-fail", gather.FromSlice([]byte("hello")), blob.PutOptions{}) + require.Error(t, err) + require.ErrorContains(t, err, "can't sync temporary file data") + + _, err = st.GetMetadata(ctx, "blob-sync-fail") + require.ErrorIs(t, err, blob.ErrBlobNotFound) +} diff --git a/repo/blob/filesystem/filesystem_storage_test.go b/repo/blob/filesystem/filesystem_storage_test.go index b814a5871db..1a451198ed2 100644 --- a/repo/blob/filesystem/filesystem_storage_test.go +++ b/repo/blob/filesystem/filesystem_storage_test.go @@ -2,9 +2,11 @@ package filesystem import ( "context" + "os" "path/filepath" "reflect" "sort" + "strings" "testing" "time" @@ -57,6 +59,74 @@ func TestFileStorage(t *testing.T) { } } +func TestFileStorageLongPath(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + + // Create a base temp directory and extend it to exceed Windows MAX_PATH (260 chars). + base := testutil.TempDirectoryShort(t) + + // Ensure the resulting path exceeds this length (slightly above 260). + const minLongPathLen = 270 + + longBase := base + if len(longBase) < minLongPathLen { + const maxSegmentLen = 60 + + // Append multiple reasonably sized subdirectories until the total path length + // exceeds minLongPathLen, avoiding a single over-long path component and + // guarding against negative repeat counts. + for len(longBase) < minLongPathLen { + remaining := minLongPathLen - len(longBase) + + // Leave room for a path separator added by filepath.Join. + segLen := maxSegmentLen + if remaining <= maxSegmentLen+1 { + segLen = remaining - 1 + } + + if segLen <= 0 { + break + } + + segment := strings.Repeat("x", segLen) + longBase = filepath.Join(longBase, segment) + } + } + + r, err := New(ctx, &Options{ + Path: longBase, + Options: sharded.Options{ + DirectoryShards: []int{2, 2}, + }, + }, true) + require.NoError(t, err) + require.NotNil(t, r) + + t.Cleanup(func() { + require.NoError(t, r.Close(testlogging.ContextForCleanup(t))) + }) + + blobID := blob.ID("testbloblongpath12345678") + data := []byte{1, 2, 3, 4, 5} + + require.NoError(t, r.PutBlob(ctx, blobID, gather.FromSlice(data), blob.PutOptions{})) + + var buf gather.WriteBuffer + defer buf.Close() + + require.NoError(t, r.GetBlob(ctx, blobID, 0, -1, &buf)) + require.Equal(t, data, buf.ToByteSlice()) + + blobs, err := blob.ListAllBlobs(ctx, r, "") + require.NoError(t, err) + require.Len(t, blobs, 1) + require.Equal(t, blobID, blobs[0].BlobID) + + require.NoError(t, r.DeleteBlob(ctx, blobID)) +} + func TestFileStorageValidate(t *testing.T) { t.Parallel() @@ -92,43 +162,41 @@ func TestFileStorageTouch(t *testing.T) { ctx := testlogging.Context(t) - path := testutil.TempDirectory(t) - r, err := New(ctx, &Options{ - Path: path, + Path: testutil.TempDirectory(t), }, true) - if r == nil || err != nil { - t.Errorf("unexpected result: %v %v", r, err) - } + require.NoError(t, err) + require.NotNil(t, r) fs := testutil.EnsureType[*fsStorage](t, r) - assertNoError(t, fs.PutBlob(ctx, t1, gather.FromSlice([]byte{1}), blob.PutOptions{})) + + require.NoError(t, fs.PutBlob(ctx, t1, gather.FromSlice([]byte{1}), blob.PutOptions{})) time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution - assertNoError(t, fs.PutBlob(ctx, t2, gather.FromSlice([]byte{1}), blob.PutOptions{})) + require.NoError(t, fs.PutBlob(ctx, t2, gather.FromSlice([]byte{1}), blob.PutOptions{})) time.Sleep(2 * time.Second) - assertNoError(t, fs.PutBlob(ctx, t3, gather.FromSlice([]byte{1}), blob.PutOptions{})) + require.NoError(t, fs.PutBlob(ctx, t3, gather.FromSlice([]byte{1}), blob.PutOptions{})) time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution verifyBlobTimestampOrder(t, fs, t1, t2, t3) _, err = fs.TouchBlob(ctx, t2, 1*time.Hour) - assertNoError(t, err) // has no effect, all timestamps are very new + require.NoError(t, err) // has no effect, all timestamps are very new verifyBlobTimestampOrder(t, fs, t1, t2, t3) time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution _, err = fs.TouchBlob(ctx, t1, 0) - assertNoError(t, err) // moves t1 to the top of the pile + require.NoError(t, err) // moves t1 to the top of the pile verifyBlobTimestampOrder(t, fs, t2, t3, t1) time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution _, err = fs.TouchBlob(ctx, t2, 0) - assertNoError(t, err) // moves t2 to the top of the pile + require.NoError(t, err) // moves t2 to the top of the pile verifyBlobTimestampOrder(t, fs, t3, t1, t2) time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution _, err = fs.TouchBlob(ctx, t1, 0) - assertNoError(t, err) // moves t1 to the top of the pile + require.NoError(t, err) // moves t1 to the top of the pile verifyBlobTimestampOrder(t, fs, t3, t2, t1) } @@ -136,12 +204,10 @@ func TestFileStorageConcurrency(t *testing.T) { t.Parallel() testutil.ProviderTest(t) - path := testutil.TempDirectory(t) - ctx := testlogging.Context(t) st, err := New(ctx, &Options{ - Path: path, + Path: testutil.TempDirectory(t), }, true) require.NoError(t, err) @@ -182,15 +248,12 @@ func TestFileStorage_GetBlob_RetriesOnReadError(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() osi.readFileRemainingErrors.Store(1) st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, @@ -213,18 +276,14 @@ func TestFileStorage_GetMetadata_RetriesOnError(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() - osi.statRemainingErrors.Store(1) - st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, + osInterfaceOverride: osi, }, true) require.NoError(t, err) @@ -232,7 +291,7 @@ func TestFileStorage_GetMetadata_RetriesOnError(t *testing.T) { require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{})) - asFsImpl(t, st).osi = osi + osi.statRemainingErrors.Store(1) _, err = st.GetMetadata(ctx, "someblob1234567812345678") require.NoError(t, err) @@ -251,75 +310,198 @@ func TestFileStorage_PutBlob_RetriesOnErrors(t *testing.T) { ctx := testlogging.Context(t) - dataDir := testutil.TempDirectory(t) - - osi := newMockOS() - - osi.createNewFileRemainingErrors.Store(3) - osi.mkdirAllRemainingErrors.Store(2) - osi.writeFileRemainingErrors.Store(3) - osi.writeFileCloseRemainingErrors.Store(2) - osi.renameRemainingErrors.Store(1) - osi.removeRemainingRetriableErrors.Store(3) - osi.chownRemainingErrors.Store(3) - osi.chtimesRemainingErrors.Store(3) + cases := []struct { + desc string + injectError func(*mockOS) + }{ + { + desc: "CreateNewFile", + injectError: func(osi *mockOS) { osi.createNewFileRemainingErrors.Store(1) }, + }, + { + desc: "Mkdir", + injectError: func(osi *mockOS) { osi.mkdirRemainingErrors.Store(1) }, + }, + { + desc: "Write", + injectError: func(osi *mockOS) { osi.writeFileRemainingErrors.Store(1) }, + }, + { + desc: "Close", + injectError: func(osi *mockOS) { osi.writeFileCloseRemainingErrors.Store(1) }, + }, + { + desc: "Rename", + injectError: func(osi *mockOS) { osi.renameRemainingErrors.Store(1) }, + }, + { + desc: "Chown", + injectError: func(osi *mockOS) { osi.chownRemainingErrors.Store(2) }, // these are ignored + }, + { + desc: "Chtimes", + injectError: func(osi *mockOS) { osi.chtimesRemainingErrors.Store(1) }, + }, + } fileUID := 3 fileGID := 4 - st, err := New(ctx, &Options{ - Path: dataDir, - FileUID: &fileUID, - FileGID: &fileGID, - Options: sharded.Options{ - DirectoryShards: []int{5, 2}, - }, - }, true) - require.NoError(t, err) + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + osi := newMockOS() - asFsImpl(t, st).osi = osi + st, err := New(ctx, &Options{ + Path: testutil.TempDirectory(t), + FileUID: &fileUID, + FileGID: &fileGID, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) - defer st.Close(ctx) + defer st.Close(ctx) - require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{})) + // create dummy blob to force creating .shards file, so it does not interfere with error injection + require.NoError(t, st.PutBlob(ctx, "dummy", gather.FromSlice([]byte{0}), blob.PutOptions{})) - var buf gather.WriteBuffer - defer buf.Close() + tc.injectError(osi) // inject error - require.NoError(t, st.GetBlob(ctx, "someblob1234567812345678", 1, 2, &buf)) - require.Equal(t, []byte{2, 3}, buf.ToByteSlice()) + require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), // exercise chtimes code path + })) - var mt time.Time + var buf gather.WriteBuffer + defer buf.Close() - require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ - GetModTime: &mt, - })) + require.NoError(t, st.GetBlob(ctx, "someblob1234567812345678", 1, 2, &buf)) + require.Equal(t, []byte{2, 3}, buf.ToByteSlice()) - require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ - SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), - })) + var mt time.Time + + require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + GetModTime: &mt, + })) + + require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), + })) + }) + } } -func TestFileStorage_DeleteBlob_ErrorHandling(t *testing.T) { +func TestFileStorage_PutBlob_DoesNotExceedRetriesOnErrors(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - dataDir := testutil.TempDirectory(t) + cases := []struct { + desc string + injectError func(*mockOS) + expectGetBlobSucceed bool + }{ + { + desc: "CreateNewFile", + injectError: func(osi *mockOS) { osi.createNewFileRemainingErrors.Store(2) }, + }, + { + desc: "Mkdir", + injectError: func(osi *mockOS) { osi.mkdirRemainingErrors.Store(2) }, + }, + + { + desc: "Write", + injectError: func(osi *mockOS) { osi.writeFileRemainingErrors.Store(2) }, + }, + + { + desc: "Close", + injectError: func(osi *mockOS) { osi.writeFileCloseRemainingErrors.Store(2) }, + }, + + { + desc: "Rename", + injectError: func(osi *mockOS) { osi.renameRemainingErrors.Store(2) }, + }, + { + desc: "Chtimes", + injectError: func(osi *mockOS) { osi.chtimesRemainingErrors.Store(2) }, + expectGetBlobSucceed: true, + }, + } + + fileUID := 3 + fileGID := 4 + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + osi := newMockOS() + + st, err := New(ctx, &Options{ + Path: testutil.TempDirectory(t), + FileUID: &fileUID, + FileGID: &fileGID, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + + defer st.Close(ctx) + + // create dummy blob to force creating .shards file, so it does not interfere with error injection + require.NoError(t, st.PutBlob(ctx, "dummy", gather.FromSlice([]byte{0}), blob.PutOptions{})) + + tc.injectError(osi) + + require.Error(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), + })) + + var buf gather.WriteBuffer + defer buf.Close() + + if err := st.GetBlob(ctx, "someblob1234567812345678", 1, 2, &buf); tc.expectGetBlobSucceed { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Zero(t, buf.Length()) + } + + var mt time.Time + + // these PutBlob calls should succeed since the injected errors are exhausted + require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + GetModTime: &mt, + })) + + require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{ + SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), + })) + }) + } +} + +func TestFileStorage_DeleteBlob_ErrorHandling(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) osi := newMockOS() osi.removeRemainingNonRetriableErrors.Store(1) st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, + osInterfaceOverride: osi, }, true) require.NoError(t, err) - asFsImpl(t, st).osi = osi - defer st.Close(ctx) require.ErrorIs(t, st.DeleteBlob(ctx, "someblob1234567812345678"), errNonRetriable) @@ -329,14 +511,11 @@ func TestFileStorage_New_MkdirAllFailureIsIgnored(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() osi.mkdirAllRemainingErrors.Store(1) st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, @@ -351,15 +530,12 @@ func TestFileStorage_New_ChecksDirectoryExistence(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() osi.statRemainingErrors.Store(1) st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, @@ -373,24 +549,20 @@ func TestFileStorage_ListBlobs_ErrorHandling(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() osi.readDirRemainingErrors.Store(3) osi.readDirRemainingFileDeletedDirEntry.Store(3) st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, + osInterfaceOverride: osi, }, true) require.NoError(t, err) - asFsImpl(t, st).osi = osi - defer st.Close(ctx) require.NoError(t, st.ListBlobs(ctx, "", func(bm blob.Metadata) error { @@ -414,21 +586,17 @@ func TestFileStorage_TouchBlob_ErrorHandling(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - - dataDir := testutil.TempDirectory(t) - osi := newMockOS() st, err := New(ctx, &Options{ - Path: dataDir, + Path: testutil.TempDirectory(t), Options: sharded.Options{ DirectoryShards: []int{5, 2}, }, + osInterfaceOverride: osi, }, true) require.NoError(t, err) - asFsImpl(t, st).osi = osi - defer st.Close(ctx) require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{})) @@ -483,16 +651,155 @@ func verifyBlobTimestampOrder(t *testing.T, st blob.Storage, want ...blob.ID) { } } -func assertNoError(t *testing.T, err error) { - t.Helper() - - if err != nil { - t.Errorf("err: %v", err) - } -} - func newMockOS() *mockOS { return &mockOS{ osInterface: realOS{}, } } + +func TestFileStorage_CreateTempFileWithData_Success(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + dataDir := testutil.TempDirectory(t) + + st, err := New(ctx, &Options{ + Path: dataDir, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + }, true) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, st.Close(ctx)) + }) + + data := gather.FromSlice([]byte{1, 2, 3, 4, 5}) + testPath := filepath.Join(dataDir, "someb", "lo", "b1234567812345678.f") + tempFile, err := asFsImpl(t, st).createTempFileWithData(testPath, data) + + require.NoError(t, err) + require.NotEmpty(t, tempFile) + + t.Cleanup(func() { + require.NoError(t, os.Remove(tempFile)) + }) + + require.Contains(t, tempFile, ".tmp.") + + // Verify temp file exists and has correct content + content, err := os.ReadFile(tempFile) + require.NoError(t, err) + require.Equal(t, []byte{1, 2, 3, 4, 5}, content) +} + +func TestFileStorage_CreateTempFileWithData_WriteError(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + dataDir := testutil.TempDirectory(t) + + osi := newMockOS() + osi.writeFileRemainingErrors.Store(1) + + st, err := New(ctx, &Options{ + Path: dataDir, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, st.Close(ctx)) + }) + + data := gather.FromSlice([]byte{1, 2, 3, 4, 5}) + testPath := filepath.Join(dataDir, "someb", "lo", "b1234567812345678.f") + tempFile, err := asFsImpl(t, st).createTempFileWithData(testPath, data) + + require.Error(t, err) + require.Contains(t, err.Error(), "can't write temporary file") + require.Empty(t, tempFile) + + // Verify temp file was removed (doesn't exist). There should be no other + // blobs with the same prefix, so listing blobs should return 0 entries. + verifyEmptyDir(t, filepath.Join(dataDir, "someb", "lo")) +} + +func TestFileStorage_CreateTempFileWithData_SyncError(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + dataDir := testutil.TempDirectory(t) + + osi := newMockOS() + osi.writeFileSyncRemainingErrors.Store(1) + + st, err := New(ctx, &Options{ + Path: dataDir, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, st.Close(ctx)) + }) + + data := gather.FromSlice([]byte{1, 2, 3, 4, 5}) + testPath := filepath.Join(dataDir, "someb", "lo", "b1234567812345678.f") + tempFile, err := asFsImpl(t, st).createTempFileWithData(testPath, data) + + require.Error(t, err) + require.Contains(t, err.Error(), "can't sync temporary file data") + require.Empty(t, tempFile) + + verifyEmptyDir(t, filepath.Join(dataDir, "someb", "lo")) +} + +func TestFileStorage_CreateTempFileWithData_CloseError(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + + dataDir := testutil.TempDirectory(t) + + osi := newMockOS() + osi.writeFileCloseRemainingErrors.Store(1) + + st, err := New(ctx, &Options{ + Path: dataDir, + Options: sharded.Options{ + DirectoryShards: []int{5, 2}, + }, + osInterfaceOverride: osi, + }, true) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, st.Close(ctx)) + }) + + data := gather.FromSlice([]byte{1, 2, 3, 4, 5}) + testPath := filepath.Join(dataDir, "someb", "lo", "b1234567812345678.f") + tempFile, err := asFsImpl(t, st).createTempFileWithData(testPath, data) + + require.Error(t, err) + require.ErrorContains(t, err, "can't close temporary file") + require.Empty(t, tempFile) + verifyEmptyDir(t, filepath.Join(dataDir, "someb", "lo")) +} + +func verifyEmptyDir(t *testing.T, dir string) { + t.Helper() + + entries, err := os.ReadDir(dir) + + require.NoError(t, err) + require.Empty(t, entries) +} diff --git a/repo/blob/filesystem/osinterface.go b/repo/blob/filesystem/osinterface.go index c0fb2568203..49e5f00a785 100644 --- a/repo/blob/filesystem/osinterface.go +++ b/repo/blob/filesystem/osinterface.go @@ -38,4 +38,6 @@ type osReadFile interface { type osWriteFile interface { io.WriteCloser + + Sync() error } diff --git a/repo/blob/filesystem/osinterface_mock_other_test.go b/repo/blob/filesystem/osinterface_mock_other_test.go index f75e5987292..d2431700466 100644 --- a/repo/blob/filesystem/osinterface_mock_other_test.go +++ b/repo/blob/filesystem/osinterface_mock_other_test.go @@ -11,7 +11,7 @@ import ( func (osi *mockOS) Stat(fname string) (fs.FileInfo, error) { if osi.statRemainingErrors.Add(-1) >= 0 { - return nil, &os.PathError{Op: "stat", Err: errors.New("underlying problem")} + return nil, &os.PathError{Op: "stat", Err: errors.New("injected stat error")} } return osi.osInterface.Stat(fname) diff --git a/repo/blob/filesystem/osinterface_mock_test.go b/repo/blob/filesystem/osinterface_mock_test.go index fc1a9d229f0..af4579960e5 100644 --- a/repo/blob/filesystem/osinterface_mock_test.go +++ b/repo/blob/filesystem/osinterface_mock_test.go @@ -13,10 +13,14 @@ import ( var errNonRetriable = errors.New("some non-retriable error") type mockOS struct { + osInterface + readFileRemainingErrors atomic.Int32 writeFileRemainingErrors atomic.Int32 writeFileCloseRemainingErrors atomic.Int32 + writeFileSyncRemainingErrors atomic.Int32 createNewFileRemainingErrors atomic.Int32 + mkdirRemainingErrors atomic.Int32 mkdirAllRemainingErrors atomic.Int32 renameRemainingErrors atomic.Int32 removeRemainingRetriableErrors atomic.Int32 @@ -34,8 +38,6 @@ type mockOS struct { // remaining syscall errnos //nolint:unused // Used with platform specific code eStaleRemainingErrors atomic.Int32 - - osInterface } func (osi *mockOS) Open(fname string) (osReadFile, error) { @@ -53,7 +55,7 @@ func (osi *mockOS) Open(fname string) (osReadFile, error) { func (osi *mockOS) Rename(oldname, newname string) error { if osi.renameRemainingErrors.Add(-1) >= 0 { - return &os.LinkError{Op: "rename", Old: oldname, New: newname, Err: errors.New("underlying problem")} + return &os.LinkError{Op: "rename", Old: oldname, New: newname, Err: errors.New("injected rename error")} } return osi.osInterface.Rename(oldname, newname) @@ -63,7 +65,7 @@ func (osi *mockOS) IsPathSeparator(c byte) bool { return os.IsPathSeparator(c) } func (osi *mockOS) ReadDir(dirname string) ([]fs.DirEntry, error) { if osi.readDirRemainingErrors.Add(-1) >= 0 { - return nil, &os.PathError{Op: "readdir", Err: errors.New("underlying problem")} + return nil, &os.PathError{Op: "readdir", Err: errors.New("injected readdir error")} } if osi.readDirRemainingNonRetriableErrors.Add(-1) >= 0 { @@ -88,7 +90,7 @@ func (osi *mockOS) ReadDir(dirname string) ([]fs.DirEntry, error) { func (osi *mockOS) Remove(fname string) error { if osi.removeRemainingRetriableErrors.Add(-1) >= 0 { - return &os.PathError{Op: "unlink", Err: errors.New("underlying problem")} + return &os.PathError{Op: "unlink", Err: errors.New("injected remove error")} } if osi.removeRemainingNonRetriableErrors.Add(-1) >= 0 { @@ -100,7 +102,7 @@ func (osi *mockOS) Remove(fname string) error { func (osi *mockOS) Chtimes(fname string, atime, mtime time.Time) error { if osi.chtimesRemainingErrors.Add(-1) >= 0 { - return &os.PathError{Op: "chtimes", Err: errors.New("underlying problem")} + return &os.PathError{Op: "chtimes", Err: errors.New("injected chtimes error")} } return osi.osInterface.Chtimes(fname, atime, mtime) @@ -108,7 +110,7 @@ func (osi *mockOS) Chtimes(fname string, atime, mtime time.Time) error { func (osi *mockOS) Chown(fname string, uid, gid int) error { if osi.chownRemainingErrors.Add(-1) >= 0 { - return &os.PathError{Op: "chown", Err: errors.New("underlying problem")} + return &os.PathError{Op: "chown", Err: errors.New("injected chown error")} } return osi.osInterface.Chown(fname, uid, gid) @@ -116,7 +118,7 @@ func (osi *mockOS) Chown(fname string, uid, gid int) error { func (osi *mockOS) CreateNewFile(fname string, perm os.FileMode) (osWriteFile, error) { if osi.createNewFileRemainingErrors.Add(-1) >= 0 { - return nil, &os.PathError{Op: "create", Err: errors.New("underlying problem")} + return nil, &os.PathError{Op: "create", Err: errors.New("injected error on CreateNewFile")} } wf, err := osi.osInterface.CreateNewFile(fname, perm) @@ -125,24 +127,36 @@ func (osi *mockOS) CreateNewFile(fname string, perm os.FileMode) (osWriteFile, e } if osi.writeFileRemainingErrors.Add(-1) >= 0 { - return writeFailureFile{wf}, nil + wf = writeFailureFile{wf} + } + + if osi.writeFileSyncRemainingErrors.Add(-1) >= 0 { + wf = syncFailureFile{wf} } if osi.writeFileCloseRemainingErrors.Add(-1) >= 0 { - return writeCloseFailureFile{wf}, nil + wf = writeCloseFailureFile{wf} } return wf, nil } func (osi *mockOS) Mkdir(fname string, mode os.FileMode) error { - if osi.mkdirAllRemainingErrors.Add(-1) >= 0 { - return &os.PathError{Op: "mkdir", Err: errors.New("underlying problem")} + if osi.mkdirRemainingErrors.Add(-1) >= 0 { + return &os.PathError{Op: "mkdir", Err: errors.New("injected mkdir error")} } return osi.osInterface.Mkdir(fname, mode) } +func (osi *mockOS) MkdirAll(fname string, mode os.FileMode) error { + if osi.mkdirAllRemainingErrors.Add(-1) >= 0 { + return &os.PathError{Op: "mkdirall", Err: errors.New("injected mkdirall error")} + } + + return osi.osInterface.MkdirAll(fname, mode) +} + func (osi *mockOS) Geteuid() int { return osi.effectiveUID } @@ -152,7 +166,7 @@ type readFailureFile struct { } func (f readFailureFile) Read(b []byte) (int, error) { - return 0, &os.PathError{Op: "read", Err: errors.New("underlying problem")} + return 0, &os.PathError{Op: "read", Err: errors.New("injected read error")} } type writeFailureFile struct { @@ -160,7 +174,15 @@ type writeFailureFile struct { } func (f writeFailureFile) Write(b []byte) (int, error) { - return 0, &os.PathError{Op: "write", Err: errors.New("underlying problem")} + return 0, &os.PathError{Op: "write", Err: errors.New("injected write error")} +} + +type syncFailureFile struct { + osWriteFile +} + +func (f syncFailureFile) Sync() error { + return &os.PathError{Op: "fsync", Err: errors.New("injected sync error")} } type writeCloseFailureFile struct { @@ -168,7 +190,12 @@ type writeCloseFailureFile struct { } func (f writeCloseFailureFile) Close() error { - return &os.PathError{Op: "close", Err: errors.New("underlying problem")} + // close the file to avoid leaking handles + if err := f.osWriteFile.Close(); err != nil { + return errors.Wrap(err, "underlying close error") + } + + return &os.PathError{Op: "close", Err: errors.New("injected close error")} } type mockDirEntryInfoError struct { diff --git a/repo/blob/filesystem/osinterface_mock_unix_test.go b/repo/blob/filesystem/osinterface_mock_unix_test.go index d2ae3143ae7..88b7ed5ae1e 100644 --- a/repo/blob/filesystem/osinterface_mock_unix_test.go +++ b/repo/blob/filesystem/osinterface_mock_unix_test.go @@ -12,7 +12,7 @@ import ( func (osi *mockOS) Stat(fname string) (fs.FileInfo, error) { if osi.statRemainingErrors.Add(-1) >= 0 { - return nil, &os.PathError{Op: "stat", Err: errors.New("underlying problem")} + return nil, &os.PathError{Op: "stat", Err: errors.New("injected stat error")} } if osi.eStaleRemainingErrors.Add(-1) >= 0 { diff --git a/repo/blob/filesystem/osinterface_realos.go b/repo/blob/filesystem/osinterface_realos.go index c28a5094c25..faf85f90219 100644 --- a/repo/blob/filesystem/osinterface_realos.go +++ b/repo/blob/filesystem/osinterface_realos.go @@ -5,13 +5,15 @@ import ( "io/fs" "os" "time" + + "github.com/kopia/kopia/internal/ospath" ) // realOS is an implementation of osInterface that uses real operating system calls. type realOS struct{} func (realOS) Open(fname string) (osReadFile, error) { - f, err := os.Open(fname) //nolint:gosec + f, err := os.Open(ospath.SafeLongFilename(fname)) if err != nil { //nolint:wrapcheck return nil, err @@ -28,12 +30,12 @@ func (realOS) IsPathSeparator(c byte) bool { return os.IsPathSeparator(c) } func (realOS) Rename(oldname, newname string) error { //nolint:wrapcheck - return os.Rename(oldname, newname) + return os.Rename(ospath.SafeLongFilename(oldname), ospath.SafeLongFilename(newname)) } func (realOS) ReadDir(dirname string) ([]fs.DirEntry, error) { //nolint:wrapcheck - return os.ReadDir(dirname) + return os.ReadDir(ospath.SafeLongFilename(dirname)) } func (realOS) IsPathError(err error) bool { @@ -50,32 +52,32 @@ func (realOS) IsLinkError(err error) bool { func (realOS) Remove(fname string) error { //nolint:wrapcheck - return os.Remove(fname) + return os.Remove(ospath.SafeLongFilename(fname)) } func (realOS) Stat(fname string) (os.FileInfo, error) { //nolint:wrapcheck - return os.Stat(fname) + return os.Stat(ospath.SafeLongFilename(fname)) } func (realOS) CreateNewFile(fname string, perm os.FileMode) (osWriteFile, error) { - //nolint:wrapcheck,gosec - return os.OpenFile(fname, os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm) + //nolint:wrapcheck + return os.OpenFile(ospath.SafeLongFilename(fname), os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm) } func (realOS) Mkdir(fname string, mode os.FileMode) error { //nolint:wrapcheck - return os.Mkdir(fname, mode) + return os.Mkdir(ospath.SafeLongFilename(fname), mode) } func (realOS) MkdirAll(fname string, mode os.FileMode) error { //nolint:wrapcheck - return os.MkdirAll(fname, mode) + return os.MkdirAll(ospath.SafeLongFilename(fname), mode) } func (realOS) Chtimes(fname string, atime, mtime time.Time) error { //nolint:wrapcheck - return os.Chtimes(fname, atime, mtime) + return os.Chtimes(ospath.SafeLongFilename(fname), atime, mtime) } func (realOS) Geteuid() int { @@ -84,7 +86,7 @@ func (realOS) Geteuid() int { func (realOS) Chown(fname string, uid, gid int) error { //nolint:wrapcheck - return os.Chown(fname, uid, gid) + return os.Chown(ospath.SafeLongFilename(fname), uid, gid) } var _ osInterface = realOS{} diff --git a/repo/blob/gcs/gcs_immu_test.go b/repo/blob/gcs/gcs_immu_test.go index 2566c06ca04..6180d1cfa83 100644 --- a/repo/blob/gcs/gcs_immu_test.go +++ b/repo/blob/gcs/gcs_immu_test.go @@ -70,7 +70,7 @@ func TestGoogleStorageImmutabilityProtection(t *testing.T) { count := getBlobCount(ctx, t, st, dummyBlob[:1]) require.Equal(t, 1, count) - cli := getGoogleCLI(t, opts.credentialsJSON) + cli := getGcsClient(t, opts.credentialsJSON) attrs, err := cli.Bucket(opts.bucket).Object(blobNameFullPath).Attrs(ctx) require.NoError(t, err) @@ -111,12 +111,12 @@ func TestGoogleStorageImmutabilityProtection(t *testing.T) { require.Equal(t, 0, count) } -// getGoogleCLI returns a separate client to verify things the Storage interface doesn't support. -func getGoogleCLI(t *testing.T, credentialsJSON []byte) *gcsclient.Client { +// getGcsClient returns a separate client to verify things the Storage interface doesn't support. +func getGcsClient(t *testing.T, credentialsJSON []byte) *gcsclient.Client { t.Helper() - ctx := context.Background() - cli, err := gcsclient.NewClient(ctx, option.WithCredentialsJSON(credentialsJSON)) + ctx := t.Context() + cli, err := gcsclient.NewClient(ctx, option.WithAuthCredentialsJSON(option.ServiceAccount, credentialsJSON)) require.NoError(t, err, "unable to create GCS client") diff --git a/repo/blob/gcs/gcs_storage.go b/repo/blob/gcs/gcs_storage.go index d32dc705f22..c86b88d835c 100644 --- a/repo/blob/gcs/gcs_storage.go +++ b/repo/blob/gcs/gcs_storage.go @@ -263,9 +263,9 @@ func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) clientOptions := []option.ClientOption{option.WithScopes(scope)} if j := opt.ServiceAccountCredentialJSON; len(j) > 0 { - clientOptions = append(clientOptions, option.WithCredentialsJSON(j)) + clientOptions = append(clientOptions, option.WithAuthCredentialsJSON(option.ServiceAccount, j)) } else if fn := opt.ServiceAccountCredentialsFile; fn != "" { - clientOptions = append(clientOptions, option.WithCredentialsFile(fn)) + clientOptions = append(clientOptions, option.WithAuthCredentialsFile(option.ServiceAccount, fn)) } cli, err := gcsclient.NewClient(ctx, clientOptions...) diff --git a/repo/blob/gcs/gcs_versioned_test.go b/repo/blob/gcs/gcs_versioned_test.go index 0378c22c2b9..026fc5e7a3b 100644 --- a/repo/blob/gcs/gcs_versioned_test.go +++ b/repo/blob/gcs/gcs_versioned_test.go @@ -11,7 +11,6 @@ import ( gcsclient "cloud.google.com/go/storage" "github.com/pkg/errors" "github.com/stretchr/testify/require" - "google.golang.org/api/option" "github.com/kopia/kopia/internal/clock" "github.com/kopia/kopia/internal/gather" @@ -271,11 +270,7 @@ func putBlobs(ctx context.Context, cli blob.Storage, blobID blob.ID, blobs []str func createBucket(t *testing.T, opts bucketOpts) { t.Helper() - ctx := context.Background() - - cli, err := gcsclient.NewClient(ctx, option.WithCredentialsJSON(opts.credentialsJSON)) - require.NoError(t, err, "unable to create GCS client") - + cli := getGcsClient(t, opts.credentialsJSON) attrs := &gcsclient.BucketAttrs{} bucketHandle := cli.Bucket(opts.bucket) @@ -285,7 +280,7 @@ func createBucket(t *testing.T, opts bucketOpts) { bucketHandle = bucketHandle.SetObjectRetention(true) } - err = bucketHandle.Create(ctx, opts.projectID, attrs) + err := bucketHandle.Create(t.Context(), opts.projectID, attrs) if err == nil { return } @@ -304,12 +299,9 @@ func createBucket(t *testing.T, opts bucketOpts) { func validateBucket(t *testing.T, opts bucketOpts) { t.Helper() - ctx := context.Background() - - cli, err := gcsclient.NewClient(ctx, option.WithCredentialsJSON(opts.credentialsJSON)) - require.NoError(t, err, "unable to create GCS client") + cli := getGcsClient(t, opts.credentialsJSON) - attrs, err := cli.Bucket(opts.bucket).Attrs(ctx) + attrs, err := cli.Bucket(opts.bucket).Attrs(t.Context()) require.NoError(t, err) if opts.isLockedBucket { diff --git a/repo/blob/gdrive/gdrive_storage.go b/repo/blob/gdrive/gdrive_storage.go index 417e42c97b1..b86bfcdfc49 100644 --- a/repo/blob/gdrive/gdrive_storage.go +++ b/repo/blob/gdrive/gdrive_storage.go @@ -547,6 +547,8 @@ func CreateDriveService(ctx context.Context, opt *Options) (*drive.Service, erro func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) { _ = isCreate + log(ctx).Warn("The GDrive storage provider is not actively tested, it may cause data loss, use at your own risk") + if opt.FolderID == "" { return nil, errors.New("folder-id must be specified") } diff --git a/repo/blob/rclone/rclone_storage.go b/repo/blob/rclone/rclone_storage.go index b59bed2b679..5fdeeb47687 100644 --- a/repo/blob/rclone/rclone_storage.go +++ b/repo/blob/rclone/rclone_storage.go @@ -248,6 +248,8 @@ func (r *rcloneStorage) runRCloneAndWaitForServerAddress(ctx context.Context, c // //nolint:funlen func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) { + log(ctx).Warn("The rclone storage provider is not actively tested, it may cause data loss, use at your own risk") + // generate directory for all temp files. td, err := os.MkdirTemp("", "kopia-rclone") if err != nil { diff --git a/repo/compression/compression_ids.go b/repo/compression/compression_ids.go index 0142f1577b5..5fd0dd2ef51 100644 --- a/repo/compression/compression_ids.go +++ b/repo/compression/compression_ids.go @@ -23,7 +23,7 @@ const ( headerPgzipBestSpeed HeaderID = 0x1301 headerPgzipBestCompression HeaderID = 0x1302 - headerLZ4Default HeaderID = 0x1400 + headerLZ4Removed HeaderID = 0x1400 // historically used for LZ4 and must not be reused. headerDeflateDefault HeaderID = 0x1500 headerDeflateBestSpeed HeaderID = 0x1501 diff --git a/repo/compression/compressor.go b/repo/compression/compressor.go index 52e10856fe9..ac10807b62e 100644 --- a/repo/compression/compressor.go +++ b/repo/compression/compressor.go @@ -32,6 +32,7 @@ var ( ByName = map[Name]Compressor{} HeaderIDToName = map[HeaderID]Name{} IsDeprecated = map[Name]bool{} + isUnsupported = map[HeaderID]bool{} ) // RegisterCompressor registers the provided compressor implementation. @@ -56,6 +57,13 @@ func RegisterDeprecatedCompressor(name Name, c Compressor) { IsDeprecated[name] = true } +func registerUnsupportedCompressor(name Name, c Compressor) { + RegisterCompressor(name, c) + + IsDeprecated[name] = true + isUnsupported[c.HeaderID()] = true +} + func compressionHeader(id HeaderID) []byte { b := make([]byte, compressionHeaderSize) binary.BigEndian.PutUint32(b, uint32(id)) @@ -81,6 +89,17 @@ func DecompressByHeader(output io.Writer, input io.Reader) error { return errors.Wrap(compressor.Decompress(output, input, false), "error decompressing") } +// IsSupported returns whether a named compression scheme is supported. +func IsSupported(name Name) bool { + c := ByName[name] + + if c == nil { + return false + } + + return !isUnsupported[c.HeaderID()] +} + func mustSucceed(err error) { impossible.PanicOnError(err) } diff --git a/repo/compression/compressor_lz4.go b/repo/compression/compressor_lz4.go index 2136a908b83..d735b88cc71 100644 --- a/repo/compression/compressor_lz4.go +++ b/repo/compression/compressor_lz4.go @@ -1,82 +1,26 @@ package compression import ( + "errors" "io" - "sync" - - "github.com/pierrec/lz4" - "github.com/pkg/errors" - - "github.com/kopia/kopia/internal/freepool" - "github.com/kopia/kopia/internal/iocopy" ) func init() { - RegisterDeprecatedCompressor("lz4", newLZ4Compressor(headerLZ4Default)) + registerUnsupportedCompressor("lz4", lz4Compressor{}) } -func newLZ4Compressor(id HeaderID) Compressor { - return &lz4Compressor{id, compressionHeader(id), sync.Pool{ - New: func() any { - return lz4.NewWriter(io.Discard) - }, - }} -} +var errLZ4NotSupported = errors.New("LZ4 compressor is not supported in recent versions of kopia, version v0.22.3 or older is needed to read legacy repositories that use the LZ4 compressor") -type lz4Compressor struct { - id HeaderID - header []byte - pool sync.Pool -} +type lz4Compressor struct{} -func (c *lz4Compressor) HeaderID() HeaderID { - return c.id +func (c lz4Compressor) HeaderID() HeaderID { + return headerLZ4Removed } -func (c *lz4Compressor) Compress(output io.Writer, input io.Reader) error { - if _, err := output.Write(c.header); err != nil { - return errors.Wrap(err, "unable to write header") - } - - //nolint:forcetypeassert - w := c.pool.Get().(*lz4.Writer) - defer c.pool.Put(w) - - w.Reset(output) - - if err := iocopy.JustCopy(w, input); err != nil { - return errors.Wrap(err, "compression error") - } - - if err := w.Close(); err != nil { - return errors.Wrap(err, "compression close error") - } - - return nil +func (c lz4Compressor) Compress(_ io.Writer, _ io.Reader) error { + return errLZ4NotSupported } -//nolint:gochecknoglobals -var lz4DecoderPool = freepool.New(func() *lz4.Reader { - return lz4.NewReader(nil) -}, func(v *lz4.Reader) { - v.Reset(nil) -}) - -func (c *lz4Compressor) Decompress(output io.Writer, input io.Reader, withHeader bool) error { - if withHeader { - if err := verifyCompressionHeader(input, c.header); err != nil { - return err - } - } - - dec := lz4DecoderPool.Take() - defer lz4DecoderPool.Return(dec) - - dec.Reset(input) - - if err := iocopy.JustCopy(output, dec); err != nil { - return errors.Wrap(err, "decompression error") - } - - return nil +func (c lz4Compressor) Decompress(_ io.Writer, _ io.Reader, _ bool) error { + return errLZ4NotSupported } diff --git a/repo/compression/compressor_test.go b/repo/compression/compressor_test.go index 024bebc82a2..a68d277a9bf 100644 --- a/repo/compression/compressor_test.go +++ b/repo/compression/compressor_test.go @@ -14,6 +14,10 @@ func TestMain(m *testing.M) { testutil.MyTestMain(m) } func TestCompressor(t *testing.T) { for id, comp := range ByHeaderID { + if isUnsupported[id] { + continue + } + t.Run(fmt.Sprintf("compressible-data-%x", id), func(t *testing.T) { // make sure all-zero data is compressed data := make([]byte, 10000) @@ -95,6 +99,10 @@ func BenchmarkCompressor(b *testing.B) { var sortedNames []Name for id := range ByName { + if !IsSupported(id) { + continue + } + sortedNames = append(sortedNames, id) } diff --git a/repo/content/committed_content_index_disk_cache.go b/repo/content/committed_content_index_disk_cache.go index 6a65908c920..d4aa2d2727f 100644 --- a/repo/content/committed_content_index_disk_cache.go +++ b/repo/content/committed_content_index_disk_cache.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/kopia/kopia/internal/blobparam" - "github.com/kopia/kopia/internal/cache" "github.com/kopia/kopia/internal/contentlog" "github.com/kopia/kopia/internal/contentlog/logparam" "github.com/kopia/kopia/internal/gather" @@ -95,31 +94,6 @@ func (c *diskCommittedContentIndexCache) addContentToCache(ctx context.Context, return nil } -func writeTempFileAtomic(dirname string, data []byte) (string, error) { - // write to a temp file to avoid race where two processes are writing at the same time. - tf, err := os.CreateTemp(dirname, "tmp") - if err != nil { - if os.IsNotExist(err) { - os.MkdirAll(dirname, cache.DirMode) //nolint:errcheck - tf, err = os.CreateTemp(dirname, "tmp") - } - } - - if err != nil { - return "", errors.Wrap(err, "can't create tmp file") - } - - if _, err := tf.Write(data); err != nil { - return "", errors.Wrap(err, "can't write to temp file") - } - - if err := tf.Close(); err != nil { - return "", errors.New("can't close tmp file") - } - - return tf.Name(), nil -} - func (c *diskCommittedContentIndexCache) expireUnused(ctx context.Context, used []blob.ID) error { contentlog.Log2(ctx, c.log, "expireUnused", blobparam.BlobIDList("except", used), diff --git a/repo/content/committed_content_index_disk_cache_windows.go b/repo/content/committed_content_index_disk_cache_windows.go index 4815dc67737..a23489032d6 100644 --- a/repo/content/committed_content_index_disk_cache_windows.go +++ b/repo/content/committed_content_index_disk_cache_windows.go @@ -31,9 +31,11 @@ func (c *diskCommittedContentIndexCache) mmapFile(ctx context.Context, filename retryCount := 0 for err != nil && retryCount < maxRetries { retryCount++ + contentlog.Log2(ctx, c.log, "retry unable to mmap.Open()", logparam.Int("retryCount", retryCount), logparam.Error("err", err)) + time.Sleep(nextDelay) nextDelay *= 2 @@ -54,9 +56,11 @@ func (c *diskCommittedContentIndexCache) mmapFile(ctx context.Context, filename if err2 := mm.Unmap(); err2 != nil { return errors.Wrapf(err2, "error unmapping index %v", filename) } + if err2 := f.Close(); err2 != nil { return errors.Wrapf(err2, "error closing index %v", filename) } + return nil }, nil } diff --git a/repo/content/index/content_id_to_bytes.go b/repo/content/index/content_id_to_bytes.go index a7e06fa4c4c..b8add38229d 100644 --- a/repo/content/index/content_id_to_bytes.go +++ b/repo/content/index/content_id_to_bytes.go @@ -2,6 +2,7 @@ package index import ( "bytes" + "fmt" ) func bytesToContentID(b []byte) ID { @@ -9,10 +10,14 @@ func bytesToContentID(b []byte) ID { return ID{} } + if len(b) > maxIDLength+1 { + panic(fmt.Sprintf("Content ID byte slice is longer than the maximum supported ID: %d", len(b))) + } + var id ID id.prefix = b[0] - id.idLen = byte(len(b) - 1) + id.idLen = uint8(len(b) - 1) //nolint:gosec // len(b) is checked above copy(id.data[0:len(b)-1], b[1:]) return id diff --git a/repo/content/index/id.go b/repo/content/index/id.go index 78312fd081d..aed97e4b548 100644 --- a/repo/content/index/id.go +++ b/repo/content/index/id.go @@ -28,11 +28,13 @@ func (p IDPrefix) ValidateSingle() error { return errors.New("invalid prefix, must be empty or a single letter between 'g' and 'z'") } +const maxIDLength = hashing.MaxHashSize + // ID is an identifier of content in content-addressable storage. // //nolint:recvcheck type ID struct { - data [hashing.MaxHashSize]byte + data [maxIDLength]byte // those 2 could be packed into one byte, but that seems like overkill prefix byte @@ -183,7 +185,7 @@ func IDFromHash(prefix IDPrefix, hash []byte) (ID, error) { id.prefix = prefix[0] } - id.idLen = byte(len(hash)) + id.idLen = uint8(len(hash)) //nolint:gosec // len(hash) is checked above copy(id.data[:], hash) return id, nil diff --git a/repo/content/write_temp_file.go b/repo/content/write_temp_file.go new file mode 100644 index 00000000000..78d7648c45c --- /dev/null +++ b/repo/content/write_temp_file.go @@ -0,0 +1,86 @@ +package content + +import ( + stderrors "errors" + "io" + "io/fs" + "os" + + "github.com/pkg/errors" + + "github.com/kopia/kopia/internal/cache" +) + +type file interface { + io.WriteCloser + Name() string + Sync() error +} + +type fsInterface interface { + CreateTemp(dir, pattern string) (file, error) + Remove(name string) error + MkdirAll(path string, perm fs.FileMode) error +} + +type localFS struct{} + +func (l localFS) CreateTemp(dir, pattern string) (file, error) { + return os.CreateTemp(dir, pattern) //nolint:wrapcheck +} + +func (l localFS) Remove(name string) error { + return os.Remove(name) //nolint:wrapcheck +} + +func (l localFS) MkdirAll(dirPath string, perm fs.FileMode) error { + return os.MkdirAll(dirPath, perm) //nolint:wrapcheck +} + +func writeTempFileAtomic(dirname string, data []byte) (filename string, err error) { + return writeTempFileAtomicImp(localFS{}, dirname, data) +} + +func writeTempFileAtomicImp(fsi fsInterface, dirname string, data []byte) (filename string, err error) { + // write to a temp file to avoid race where two processes are writing at the same time. + tf, err2 := fsi.CreateTemp(dirname, "tmp") + if err2 != nil { + if os.IsNotExist(err2) { + if mdErr := fsi.MkdirAll(dirname, cache.DirMode); mdErr != nil { + return "", stderrors.Join(errors.Wrap(mdErr, "cannot create parent directory for temp file"), + errors.Wrap(err2, "cannot create temp file")) + } + + tf, err2 = fsi.CreateTemp(dirname, "tmp") + } + } + + if err2 != nil { + return "", errors.Wrap(err2, "can't create tmp file") + } + + defer func() { + if cerr := tf.Close(); cerr != nil { + err = stderrors.Join(err, errors.Wrap(cerr, "can't close tmp file")) + } + + if err != nil { + // remove tmp file on error to avoid leaving them behind + if rerr := fsi.Remove(tf.Name()); rerr != nil { + err = stderrors.Join(err, errors.Wrap(rerr, "can't remove tmp file")) + } + + filename = "" + } + }() + + if _, err2 := tf.Write(data); err2 != nil { + return "", errors.Wrap(err2, "can't write to temp file") + } + + if err2 := tf.Sync(); err2 != nil { + return "", errors.Wrapf(err2, "cannot sync temporary file in dir %s", dirname) + } + + return tf.Name(), nil +} diff --git a/repo/content/write_temp_file_test.go b/repo/content/write_temp_file_test.go new file mode 100644 index 00000000000..e57bd9a3c2a --- /dev/null +++ b/repo/content/write_temp_file_test.go @@ -0,0 +1,253 @@ +package content + +import ( + "os" + "path/filepath" + "runtime" + "sync/atomic" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +// TestWriteTempFileAtomic_HappyPath verifies that writeTempFileAtomic writes +// the expected content and returns a valid file path. +func TestWriteTempFileAtomic_HappyPath(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + data := []byte("index-blob-content") + + name, err := writeTempFileAtomicImp(localFS{}, dir, data) + require.NoError(t, err) + require.NotEmpty(t, name) + + // File must exist under the given directory. + require.Equal(t, dir, filepath.Dir(name)) + + got, err := os.ReadFile(name) + require.NoError(t, err) + require.Equal(t, data, got) +} + +// TestWriteTempFileAtomic_EmptyData verifies that an empty payload is written +// without error and produces a valid (zero-byte) file. +func TestWriteTempFileAtomic_EmptyData(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + name, err := writeTempFileAtomicImp(localFS{}, dir, []byte{}) + require.NoError(t, err) + + info, err := os.Stat(name) + require.NoError(t, err) + require.EqualValues(t, 0, info.Size()) +} + +// TestWriteTempFileAtomic_CreatesDirectoryIfMissing verifies that +// writeTempFileAtomic creates the target directory when it does not exist, +// matching the MkdirAll fallback path. +func TestWriteTempFileAtomic_CreatesDirectoryIfMissing(t *testing.T) { + t.Parallel() + + // Use a path that does not yet exist. + dir := filepath.Join(t.TempDir(), "new", "nested", "dir") + + data := []byte("hello") + + name, err := writeTempFileAtomicImp(localFS{}, dir, data) + require.NoError(t, err) + require.Equal(t, dir, filepath.Dir(name)) + + got, err := os.ReadFile(name) + require.NoError(t, err) + require.Equal(t, data, got) +} + +// TestWriteTempFileAtomic_NonExistentDirUnwritable verifies that an error is +// returned when the directory cannot be created (e.g. parent is read-only). +// Skipped on platforms where root may bypass permissions. +func TestWriteTempFileAtomic_NonExistentDirUnwritable(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("does not work on windows due to chmod") + } + + if os.Getuid() == 0 { + t.Skip("skipping permission test when running as root") + } + + t.Parallel() + + // Create a read-only parent so that MkdirAll cannot create the child. + parent := t.TempDir() + require.NoError(t, os.Chmod(parent, 0o555)) + + t.Cleanup(func() { os.Chmod(parent, 0o755) }) //nolint:errcheck + + dir := filepath.Join(parent, "child") + + _, err := writeTempFileAtomicImp(localFS{}, dir, []byte("data")) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot create parent directory for temp file") +} + +type mockFileSynced struct { + file + + synced atomic.Bool +} + +func (mf *mockFileSynced) Write(p []byte) (n int, err error) { + mf.synced.Store(false) + + return mf.file.Write(p) +} + +func (mf *mockFileSynced) Sync() error { + err := mf.file.Sync() + if err == nil { + mf.synced.Store(true) + } + + return err +} + +// TestWriteTempFileAtomic_FileIsSynced verifies that Sync is called after +// writing data to the temporary file. +func TestWriteTempFileAtomic_FileIsSynced(t *testing.T) { + t.Parallel() + + var mockedFile mockFileSynced + + dir := t.TempDir() + data := []byte("synced-content") + + mfs := mockfs{ + createWrapper: func(f file) file { + mockedFile.file = f + + return &mockedFile + }, + } + + name, err := writeTempFileAtomicImp(mfs, dir, data) + require.NoError(t, err) + require.True(t, mockedFile.synced.Load()) + + // Open a new handle to avoid OS read-cache of the same descriptor. + b, err := os.ReadFile(name) + require.NoError(t, err) + require.Equal(t, data, b) +} + +// TestWriteTempFileAtomic_NoTempFilesLeft verifies that writeTempFileAtomic +// does not leak the temporary file after a successful call — the caller is +// expected to rename it, but the file descriptor must already be closed. +// We confirm this indirectly: the returned path must be stat-able (file +// exists and is closed) with no other tmp* siblings beyond the returned one. +func TestWriteTempFileAtomic_NoTempFilesLeft(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + name, err := writeTempFileAtomicImp(localFS{}, dir, []byte("data")) + require.NoError(t, err) + + entries, err := os.ReadDir(dir) + require.NoError(t, err) + + // Only one file should exist: the one returned. + require.Len(t, entries, 1) + require.Equal(t, filepath.Base(name), entries[0].Name()) +} + +type mockfs struct { + localFS + + createWrapper func(file) file +} + +func (m mockfs) CreateTemp(dir, pattern string) (file, error) { + f, err := m.localFS.CreateTemp(dir, pattern) + + if m.createWrapper != nil { + f = m.createWrapper(f) + } + + return f, err +} + +type mockFileWriteError struct { + file +} + +func (mf mockFileWriteError) Write(p []byte) (n int, err error) { + return 0, errors.New("mock file write error") +} + +type mockFileSyncError struct { + file +} + +func (mf mockFileSyncError) Sync() error { + return errors.New("mock file sync error") +} + +type mockFileCloseError struct { + file +} + +func (mf mockFileCloseError) Close() error { + if err := mf.file.Close(); err != nil { + return err + } + + return errors.New("mock file close error") +} + +// TestWriteTempFileAtomic_NoTempFilesLeftOnError verifies that writeTempFileAtomic +// does not leak the temporary file after a write, sync, or close error. +func TestWriteTempFileAtomic_NoTempFilesLeftOnError(t *testing.T) { + t.Parallel() + + cases := []struct { + mockfs + description string + }{ + { + description: "write-error", + mockfs: mockfs{ + createWrapper: func(f file) file { return mockFileWriteError{file: f} }, + }, + }, + { + description: "sync-error", + mockfs: mockfs{ + createWrapper: func(f file) file { return mockFileSyncError{file: f} }, + }, + }, + { + description: "close-error", + mockfs: mockfs{ + createWrapper: func(f file) file { return mockFileCloseError{file: f} }, + }, + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + dir := t.TempDir() + + name, err := writeTempFileAtomicImp(c.mockfs, dir, []byte("data")) + require.Error(t, err) + require.Empty(t, name) + t.Log("error:", err) + + entries, err := os.ReadDir(dir) + require.NoError(t, err) + require.Empty(t, entries) + }) + } +} diff --git a/repo/format/format_blob_test.go b/repo/format/format_blob_test.go index ab454ea97bf..c917d79f5a8 100644 --- a/repo/format/format_blob_test.go +++ b/repo/format/format_blob_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/pkg/errors" + "github.com/stretchr/testify/assert" "github.com/kopia/kopia/internal/blobtesting" "github.com/kopia/kopia/internal/gather" @@ -29,19 +30,19 @@ func TestFormatBlobRecovery(t *testing.T) { t.Errorf("unexpected checksummed length: %v, want %v", got, want) } - assertNoError(t, st.PutBlob(ctx, "some-blob-by-itself", gather.FromSlice(checksummed), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "some-blob-suffix", gather.FromSlice(append(append([]byte(nil), 1, 2, 3), checksummed...)), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "some-blob-prefix", gather.FromSlice(append(append([]byte(nil), checksummed...), 1, 2, 3)), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "some-blob-by-itself", gather.FromSlice(checksummed), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "some-blob-suffix", gather.FromSlice(append(append([]byte(nil), 1, 2, 3), checksummed...)), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "some-blob-prefix", gather.FromSlice(append(append([]byte(nil), checksummed...), 1, 2, 3)), blob.PutOptions{})) // mess up checksum checksummed[len(checksummed)-3] ^= 1 - assertNoError(t, st.PutBlob(ctx, "bad-checksum", gather.FromSlice(checksummed), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "zero-len", gather.FromSlice([]byte{}), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "one-len", gather.FromSlice([]byte{1}), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "two-len", gather.FromSlice([]byte{1, 2}), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "three-len", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "four-len", gather.FromSlice([]byte{1, 2, 3, 4}), blob.PutOptions{})) - assertNoError(t, st.PutBlob(ctx, "five-len", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "bad-checksum", gather.FromSlice(checksummed), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "zero-len", gather.FromSlice([]byte{}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "one-len", gather.FromSlice([]byte{1}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "two-len", gather.FromSlice([]byte{1, 2}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "three-len", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "four-len", gather.FromSlice([]byte{1, 2, 3, 4}), blob.PutOptions{})) + assert.NoError(t, st.PutBlob(ctx, "five-len", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{})) cases := []struct { blobID blob.ID @@ -75,11 +76,3 @@ func TestFormatBlobRecovery(t *testing.T) { }) } } - -func assertNoError(t *testing.T, err error) { - t.Helper() - - if err != nil { - t.Errorf("err: %v", err) - } -} diff --git a/repo/maintenance/blob_retain.go b/repo/maintenance/blob_retain.go index 8c45bf541ea..89f5bf5c880 100644 --- a/repo/maintenance/blob_retain.go +++ b/repo/maintenance/blob_retain.go @@ -56,7 +56,7 @@ func extendBlobRetentionTime(ctx context.Context, rep repo.DirectRepositoryWrite var ( wg errgroup.Group - extendedCount, toExtend, failedCount atomic.Uint32 + extendedCount, toExtend, failedCount atomic.Uint64 ) if opt.Parallel == 0 { @@ -79,7 +79,7 @@ func extendBlobRetentionTime(ctx context.Context, rep repo.DirectRepositoryWrite } if currentCount := extendedCount.Add(1); currentCount%100 == 0 { - contentlog.Log1(ctx, log, "extended blobs", logparam.UInt32("count", currentCount)) + contentlog.Log1(ctx, log, "extended blobs", logparam.UInt64("count", currentCount)) } } @@ -100,7 +100,7 @@ func extendBlobRetentionTime(ctx context.Context, rep repo.DirectRepositoryWrite close(extend) - contentlog.Log1(ctx, log, "Found blobs to extend", logparam.UInt32("count", toExtend.Load())) + contentlog.Log1(ctx, log, "Found blobs to extend", logparam.UInt64("count", toExtend.Load())) errWait := wg.Wait() // wait for all extend workers to finish. impossible.PanicOnError(errWait) diff --git a/repo/maintenance/blob_retain_test.go b/repo/maintenance/blob_retain_test.go index 4d78cb16e7a..154fc533678 100644 --- a/repo/maintenance/blob_retain_test.go +++ b/repo/maintenance/blob_retain_test.go @@ -73,8 +73,8 @@ func (s *formatSpecificTestSuite) TestExtendBlobRetentionTime(t *testing.T) { // extend retention time of all blobs stats, err := maintenance.ExtendBlobRetentionTime(ctx, env.RepositoryWriter, maintenance.ExtendBlobRetentionTimeOptions{}) require.NoError(t, err) - require.Equal(t, uint32(4), stats.ToExtendBlobCount) - require.Equal(t, uint32(4), stats.ExtendedBlobCount) + require.EqualValues(t, 4, stats.ToExtendBlobCount) + require.EqualValues(t, 4, stats.ExtendedBlobCount) require.Equal(t, "24h0m0s", stats.RetentionPeriod) gotMode, expiry, err = st.GetRetention(ctx, blobsBefore[lastBlobIdx].BlobID) diff --git a/repo/maintenance/cleanup_logs.go b/repo/maintenance/cleanup_logs.go index fb74131c64e..ad1930c3fdd 100644 --- a/repo/maintenance/cleanup_logs.go +++ b/repo/maintenance/cleanup_logs.go @@ -64,14 +64,14 @@ func CleanupLogs(ctx context.Context, rep repo.DirectRepositoryWriter, opt LogRe return allLogBlobs[i].Timestamp.After(allLogBlobs[j].Timestamp) }) - var retainedSize int64 + var retainedSize uint64 deletePosition := len(allLogBlobs) for i, bm := range allLogBlobs { - retainedSize += bm.Length + bmlen := maintenancestats.ToUint64(bm.Length) - if retainedSize > opt.MaxTotalSize && opt.MaxTotalSize > 0 { + if opt.MaxTotalSize > 0 && retainedSize+bmlen > uint64(opt.MaxTotalSize) { deletePosition = i break } @@ -85,19 +85,21 @@ func CleanupLogs(ctx context.Context, rep repo.DirectRepositoryWriter, opt LogRe deletePosition = i break } + + retainedSize += bmlen } toDelete := allLogBlobs[deletePosition:] - var toDeleteSize int64 + var toDeleteSize uint64 for _, bm := range toDelete { - toDeleteSize += bm.Length + toDeleteSize += maintenancestats.ToUint64(bm.Length) } result := &maintenancestats.CleanupLogsStats{ - RetainedBlobCount: deletePosition, + RetainedBlobCount: maintenancestats.ToUint64(deletePosition), RetainedBlobSize: retainedSize, - ToDeleteBlobCount: len(toDelete), + ToDeleteBlobCount: maintenancestats.ToUint64(len(toDelete)), ToDeleteBlobSize: toDeleteSize, DeletedBlobCount: 0, DeletedBlobSize: 0, diff --git a/repo/maintenance/content_rewrite.go b/repo/maintenance/content_rewrite.go index c94ad11175d..95d4fa5e6f5 100644 --- a/repo/maintenance/content_rewrite.go +++ b/repo/maintenance/content_rewrite.go @@ -142,12 +142,12 @@ func RewriteContents(ctx context.Context, rep repo.DirectRepositoryWriter, opt * rewrittenCount, rewrittenBytes := rewritten.Approximate() result := &maintenancestats.RewriteContentsStats{ - ToRewriteContentCount: int(toRewriteCount), - ToRewriteContentSize: toRewriteBytes, - RewrittenContentCount: int(rewrittenCount), - RewrittenContentSize: rewrittenBytes, - RetainedContentCount: int(retainedCount), - RetainedContentSize: retainedBytes, + ToRewriteContentCount: uint64(toRewriteCount), + ToRewriteContentSize: maintenancestats.ToUint64(toRewriteBytes), + RewrittenContentCount: uint64(rewrittenCount), + RewrittenContentSize: maintenancestats.ToUint64(rewrittenBytes), + RetainedContentCount: uint64(retainedCount), + RetainedContentSize: maintenancestats.ToUint64(retainedBytes), } contentlog.Log1(ctx, log, "Rewritten contents", result) diff --git a/repo/maintenance/maintenance_run.go b/repo/maintenance/maintenance_run.go index ac942b67a30..c733cea05eb 100644 --- a/repo/maintenance/maintenance_run.go +++ b/repo/maintenance/maintenance_run.go @@ -334,7 +334,7 @@ func runTaskCleanupLogs(ctx context.Context, runParams RunParameters, s *Schedul return ReportRun(ctx, runParams.rep, TaskCleanupLogs, s, func() (maintenancestats.Kind, error) { stats, err := CleanupLogs(ctx, runParams.rep, runParams.Params.LogRetention.OrDefault()) - var deletedLogCount int + var deletedLogCount uint64 if stats != nil { deletedLogCount = stats.DeletedBlobCount } diff --git a/repo/maintenance/pack_gc.go b/repo/maintenance/pack_gc.go index f2e13ed52c3..9103fdae73d 100644 --- a/repo/maintenance/pack_gc.go +++ b/repo/maintenance/pack_gc.go @@ -147,10 +147,10 @@ func DeleteUnreferencedPacks(ctx context.Context, rep repo.DirectRepositoryWrite retainedCount, retainedSize := retained.Approximate() result := &maintenancestats.DeleteUnreferencedPacksStats{ - UnreferencedPackCount: unreferencedCount, - UnreferencedTotalSize: unreferencedSize, - RetainedPackCount: retainedCount, - RetainedTotalSize: retainedSize, + UnreferencedPackCount: uint64(unreferencedCount), + UnreferencedTotalSize: maintenancestats.ToUint64(unreferencedSize), + RetainedPackCount: uint64(retainedCount), + RetainedTotalSize: maintenancestats.ToUint64(retainedSize), DeletedPackCount: 0, DeletedTotalSize: 0, } @@ -167,8 +167,8 @@ func DeleteUnreferencedPacks(ctx context.Context, rep repo.DirectRepositoryWrite } deletedCount, deletedSize := deleted.Approximate() - result.DeletedPackCount = deletedCount - result.DeletedTotalSize = deletedSize + result.DeletedPackCount = uint64(deletedCount) + result.DeletedTotalSize = maintenancestats.ToUint64(deletedSize) contentlog.Log1(ctx, log, "Completed deleting unreferenced pack blobs", result) diff --git a/repo/maintenancestats/stats_advance_epoch.go b/repo/maintenancestats/stats_advance_epoch.go index 6f35a7dfe8d..0854533f170 100644 --- a/repo/maintenancestats/stats_advance_epoch.go +++ b/repo/maintenancestats/stats_advance_epoch.go @@ -10,14 +10,14 @@ const advanceEpochStatsKind = "advanceEpochStats" // AdvanceEpochStats are the stats for advancing write epoch. type AdvanceEpochStats struct { - CurrentEpoch int `json:"currentEpoch"` - WasAdvanced bool `json:"wasAdvanced"` + CurrentEpoch uint64 `json:"currentEpoch"` + WasAdvanced bool `json:"wasAdvanced"` } // WriteValueTo writes the stats to JSONWriter. func (as *AdvanceEpochStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(as.Kind()) - jw.IntField("currentEpoch", as.CurrentEpoch) + jw.UInt64Field("currentEpoch", as.CurrentEpoch) jw.BoolField("wasAdvanced", as.WasAdvanced) jw.EndObject() } diff --git a/repo/maintenancestats/stats_clean_up_log.go b/repo/maintenancestats/stats_clean_up_log.go index eca79270bc0..a68464ac88c 100644 --- a/repo/maintenancestats/stats_clean_up_log.go +++ b/repo/maintenancestats/stats_clean_up_log.go @@ -4,35 +4,39 @@ import ( "fmt" "github.com/kopia/kopia/internal/contentlog" + "github.com/kopia/kopia/internal/units" ) const cleanupLogsStatsKind = "cleanupLogsStats" -// CleanupLogsStats are the stats for cleanning up logs. +// CleanupLogsStats are the stats for cleaning up logs. type CleanupLogsStats struct { - ToDeleteBlobCount int `json:"toDeleteBlobCount"` - ToDeleteBlobSize int64 `json:"toDeleteBlobSize"` - DeletedBlobCount int `json:"deletedBlobCount"` - DeletedBlobSize int64 `json:"deletedBlobSize"` - RetainedBlobCount int `json:"retainedBlobCount"` - RetainedBlobSize int64 `json:"retainedBlobSize"` + ToDeleteBlobCount uint64 `json:"toDeleteBlobCount"` + ToDeleteBlobSize uint64 `json:"toDeleteBlobSize"` + DeletedBlobCount uint64 `json:"deletedBlobCount"` + DeletedBlobSize uint64 `json:"deletedBlobSize"` + RetainedBlobCount uint64 `json:"retainedBlobCount"` + RetainedBlobSize uint64 `json:"retainedBlobSize"` } // WriteValueTo writes the stats to JSONWriter. func (cs *CleanupLogsStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(cs.Kind()) - jw.IntField("toDeleteBlobCount", cs.ToDeleteBlobCount) - jw.Int64Field("toDeleteBlobSize", cs.ToDeleteBlobSize) - jw.IntField("deletedBlobCount", cs.DeletedBlobCount) - jw.Int64Field("deletedBlobSize", cs.DeletedBlobSize) - jw.IntField("retainedBlobCount", cs.RetainedBlobCount) - jw.Int64Field("retainedBlobSize", cs.RetainedBlobSize) + jw.UInt64Field("toDeleteBlobCount", cs.ToDeleteBlobCount) + jw.UInt64Field("toDeleteBlobSize", cs.ToDeleteBlobSize) + jw.UInt64Field("deletedBlobCount", cs.DeletedBlobCount) + jw.UInt64Field("deletedBlobSize", cs.DeletedBlobSize) + jw.UInt64Field("retainedBlobCount", cs.RetainedBlobCount) + jw.UInt64Field("retainedBlobSize", cs.RetainedBlobSize) jw.EndObject() } // Summary generates a human readable summary for the stats. func (cs *CleanupLogsStats) Summary() string { - return fmt.Sprintf("Found %v(%v) logs blobs for deletion and deleted %v(%v) of them. Retained %v(%v) log blobs.", cs.ToDeleteBlobCount, cs.ToDeleteBlobSize, cs.DeletedBlobCount, cs.DeletedBlobSize, cs.RetainedBlobCount, cs.RetainedBlobSize) + return fmt.Sprintf("Found %v(%v) logs blobs for deletion and deleted %v(%v) of them. Retained %v(%v) log blobs.", + cs.ToDeleteBlobCount, units.BytesString(cs.ToDeleteBlobSize), cs.DeletedBlobCount, + units.BytesString(cs.DeletedBlobSize), cs.RetainedBlobCount, + units.BytesString(cs.RetainedBlobSize)) } // Kind returns the kind name for the stats. diff --git a/repo/maintenancestats/stats_cleanup_markers.go b/repo/maintenancestats/stats_cleanup_markers.go index f212f719330..0b45897ca4e 100644 --- a/repo/maintenancestats/stats_cleanup_markers.go +++ b/repo/maintenancestats/stats_cleanup_markers.go @@ -10,15 +10,15 @@ const cleanupMarkersStatsKind = "cleanupMarkersStats" // CleanupMarkersStats are the stats for cleaning up markers. type CleanupMarkersStats struct { - DeletedEpochMarkerBlobCount int `json:"deletedEpochMarkerBlobCount"` - DeletedWatermarkBlobCount int `json:"deletedWatermarkBlobCount"` + DeletedEpochMarkerBlobCount uint64 `json:"deletedEpochMarkerBlobCount"` + DeletedWatermarkBlobCount uint64 `json:"deletedWatermarkBlobCount"` } // WriteValueTo writes the stats to JSONWriter. func (cs *CleanupMarkersStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(cs.Kind()) - jw.IntField("deletedEpochMarkerBlobCount", cs.DeletedEpochMarkerBlobCount) - jw.IntField("deletedWatermarkBlobCount", cs.DeletedWatermarkBlobCount) + jw.UInt64Field("deletedEpochMarkerBlobCount", cs.DeletedEpochMarkerBlobCount) + jw.UInt64Field("deletedWatermarkBlobCount", cs.DeletedWatermarkBlobCount) jw.EndObject() } diff --git a/repo/maintenancestats/stats_cleanup_superseded_indexes.go b/repo/maintenancestats/stats_cleanup_superseded_indexes.go index dff8bc788e0..ff8485ec0db 100644 --- a/repo/maintenancestats/stats_cleanup_superseded_indexes.go +++ b/repo/maintenancestats/stats_cleanup_superseded_indexes.go @@ -5,6 +5,7 @@ import ( "time" "github.com/kopia/kopia/internal/contentlog" + "github.com/kopia/kopia/internal/units" ) const cleanupSupersededIndexesStatsKind = "cleanupSupersededIndexesStats" @@ -12,22 +13,22 @@ const cleanupSupersededIndexesStatsKind = "cleanupSupersededIndexesStats" // CleanupSupersededIndexesStats are the stats for cleaning up superseded indexes. type CleanupSupersededIndexesStats struct { MaxReplacementTime time.Time `json:"maxReplacementTime"` - DeletedBlobCount int `json:"deletedBlobCount"` - DeletedTotalSize int64 `json:"deletedTotalSize"` + DeletedBlobCount uint64 `json:"deletedBlobCount"` + DeletedTotalSize uint64 `json:"deletedTotalSize"` } // WriteValueTo writes the stats to JSONWriter. func (cs *CleanupSupersededIndexesStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(cs.Kind()) jw.TimeField("maxReplacementTime", cs.MaxReplacementTime) - jw.IntField("deletedBlobCount", cs.DeletedBlobCount) - jw.Int64Field("deletedTotalSize", cs.DeletedTotalSize) + jw.UInt64Field("deletedBlobCount", cs.DeletedBlobCount) + jw.UInt64Field("deletedTotalSize", cs.DeletedTotalSize) jw.EndObject() } // Summary generates a human readable summary for the stats. func (cs *CleanupSupersededIndexesStats) Summary() string { - return fmt.Sprintf("Cleaned up %v(%v) superseded index blobs, max replacement time %v", cs.DeletedBlobCount, cs.DeletedTotalSize, cs.MaxReplacementTime) + return fmt.Sprintf("Cleaned up %v(%v) superseded index blobs, max replacement time %v", cs.DeletedBlobCount, units.BytesString(cs.DeletedTotalSize), cs.MaxReplacementTime) } // Kind returns the kind name for the stats. diff --git a/repo/maintenancestats/stats_compact_single_epoch.go b/repo/maintenancestats/stats_compact_single_epoch.go index 1dfc930648c..616f1624751 100644 --- a/repo/maintenancestats/stats_compact_single_epoch.go +++ b/repo/maintenancestats/stats_compact_single_epoch.go @@ -4,29 +4,30 @@ import ( "fmt" "github.com/kopia/kopia/internal/contentlog" + "github.com/kopia/kopia/internal/units" ) const compactSingleEpochStatsKind = "compactSingleEpochStats" // CompactSingleEpochStats are the stats for compacting an index epoch. type CompactSingleEpochStats struct { - SupersededIndexBlobCount int `json:"supersededIndexBlobCount"` - SupersededIndexTotalSize int64 `json:"supersededIndexTotalSize"` - Epoch int `json:"epoch"` + SupersededIndexBlobCount uint64 `json:"supersededIndexBlobCount"` + SupersededIndexTotalSize uint64 `json:"supersededIndexTotalSize"` + Epoch uint64 `json:"epoch"` } // WriteValueTo writes the stats to JSONWriter. func (cs *CompactSingleEpochStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(cs.Kind()) - jw.IntField("supersededIndexBlobCount", cs.SupersededIndexBlobCount) - jw.Int64Field("supersededIndexTotalSize", cs.SupersededIndexTotalSize) - jw.IntField("epoch", cs.Epoch) + jw.UInt64Field("supersededIndexBlobCount", cs.SupersededIndexBlobCount) + jw.UInt64Field("supersededIndexTotalSize", cs.SupersededIndexTotalSize) + jw.UInt64Field("epoch", cs.Epoch) jw.EndObject() } // Summary generates a human readable summary for the stats. func (cs *CompactSingleEpochStats) Summary() string { - return fmt.Sprintf("Compacted %v(%v) index blobs for epoch %v", cs.SupersededIndexBlobCount, cs.SupersededIndexTotalSize, cs.Epoch) + return fmt.Sprintf("Compacted %v(%v) index blobs for epoch %v", cs.SupersededIndexBlobCount, units.BytesString(cs.SupersededIndexTotalSize), cs.Epoch) } // Kind returns the kind name for the stats. diff --git a/repo/maintenancestats/stats_delete_unreferenced_packs.go b/repo/maintenancestats/stats_delete_unreferenced_packs.go index d20a1a2f061..32a6d6e9b2e 100644 --- a/repo/maintenancestats/stats_delete_unreferenced_packs.go +++ b/repo/maintenancestats/stats_delete_unreferenced_packs.go @@ -4,36 +4,37 @@ import ( "fmt" "github.com/kopia/kopia/internal/contentlog" + "github.com/kopia/kopia/internal/units" ) const deleteUnreferencedPacksStatsKind = "deleteUnreferencedPacksStats" // DeleteUnreferencedPacksStats are the stats for deleting unreferenced packs. type DeleteUnreferencedPacksStats struct { - UnreferencedPackCount uint32 `json:"unreferencedPackCount"` - UnreferencedTotalSize int64 `json:"unreferencedTotalSize"` - DeletedPackCount uint32 `json:"deletedPackCount"` - DeletedTotalSize int64 `json:"deletedTotalSize"` - RetainedPackCount uint32 `json:"retainedPackCount"` - RetainedTotalSize int64 `json:"retainedTotalSize"` + UnreferencedPackCount uint64 `json:"unreferencedPackCount"` + UnreferencedTotalSize uint64 `json:"unreferencedTotalSize"` + DeletedPackCount uint64 `json:"deletedPackCount"` + DeletedTotalSize uint64 `json:"deletedTotalSize"` + RetainedPackCount uint64 `json:"retainedPackCount"` + RetainedTotalSize uint64 `json:"retainedTotalSize"` } // WriteValueTo writes the stats to JSONWriter. func (ds *DeleteUnreferencedPacksStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(ds.Kind()) - jw.UInt32Field("unreferencedPackCount", ds.UnreferencedPackCount) - jw.Int64Field("unreferencedTotalSize", ds.UnreferencedTotalSize) - jw.UInt32Field("deletedPackCount", ds.DeletedPackCount) - jw.Int64Field("deletedTotalSize", ds.DeletedTotalSize) - jw.UInt32Field("retainedPackCount", ds.RetainedPackCount) - jw.Int64Field("retainedTotalSize", ds.RetainedTotalSize) + jw.UInt64Field("unreferencedPackCount", ds.UnreferencedPackCount) + jw.UInt64Field("unreferencedTotalSize", ds.UnreferencedTotalSize) + jw.UInt64Field("deletedPackCount", ds.DeletedPackCount) + jw.UInt64Field("deletedTotalSize", ds.DeletedTotalSize) + jw.UInt64Field("retainedPackCount", ds.RetainedPackCount) + jw.UInt64Field("retainedTotalSize", ds.RetainedTotalSize) jw.EndObject() } // Summary generates a human readable summary for the stats. func (ds *DeleteUnreferencedPacksStats) Summary() string { return fmt.Sprintf("Found %v(%v) unreferenced pack blobs to delete and deleted %v(%v). Retained %v(%v) unreferenced pack blobs.", - ds.UnreferencedPackCount, ds.UnreferencedTotalSize, ds.DeletedPackCount, ds.DeletedTotalSize, ds.RetainedPackCount, ds.RetainedTotalSize) + ds.UnreferencedPackCount, units.BytesString(ds.UnreferencedTotalSize), ds.DeletedPackCount, units.BytesString(ds.DeletedTotalSize), ds.RetainedPackCount, units.BytesString(ds.RetainedTotalSize)) } // Kind returns the kind name for the stats. diff --git a/repo/maintenancestats/stats_extend_blob_retention.go b/repo/maintenancestats/stats_extend_blob_retention.go index 0663aa94f19..960c1dbbacd 100644 --- a/repo/maintenancestats/stats_extend_blob_retention.go +++ b/repo/maintenancestats/stats_extend_blob_retention.go @@ -10,16 +10,16 @@ const extendBlobRetentionStatsKind = "extendBlobRetentionStats" // ExtendBlobRetentionStats are the stats for extending blob retention time. type ExtendBlobRetentionStats struct { - ToExtendBlobCount uint32 `json:"toExtendBlobCount"` - ExtendedBlobCount uint32 `json:"extendedBlobCount"` + ToExtendBlobCount uint64 `json:"toExtendBlobCount"` + ExtendedBlobCount uint64 `json:"extendedBlobCount"` RetentionPeriod string `json:"retentionPeriod"` } // WriteValueTo writes the stats to JSONWriter. func (es *ExtendBlobRetentionStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(es.Kind()) - jw.UInt32Field("toExtendBlobCount", es.ToExtendBlobCount) - jw.UInt32Field("extendedBlobCount", es.ExtendedBlobCount) + jw.UInt64Field("toExtendBlobCount", es.ToExtendBlobCount) + jw.UInt64Field("extendedBlobCount", es.ExtendedBlobCount) jw.StringField("retentionPeriod", es.RetentionPeriod) jw.EndObject() } diff --git a/repo/maintenancestats/stats_generate_range_checkpoint.go b/repo/maintenancestats/stats_generate_range_checkpoint.go index d8b6c7fa890..dc4efc656b8 100644 --- a/repo/maintenancestats/stats_generate_range_checkpoint.go +++ b/repo/maintenancestats/stats_generate_range_checkpoint.go @@ -10,15 +10,15 @@ const generateRangeCheckpointStatsKind = "generateRangeCheckpointStats" // GenerateRangeCheckpointStats are the stats for generating range checkpoints. type GenerateRangeCheckpointStats struct { - RangeMinEpoch int `json:"rangeMinEpoch"` - RangeMaxEpoch int `json:"rangeMaxEpoch"` + RangeMinEpoch uint64 `json:"rangeMinEpoch"` + RangeMaxEpoch uint64 `json:"rangeMaxEpoch"` } // WriteValueTo writes the stats to JSONWriter. func (gs *GenerateRangeCheckpointStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(gs.Kind()) - jw.IntField("rangeMinEpoch", gs.RangeMinEpoch) - jw.IntField("rangeMaxEpoch", gs.RangeMaxEpoch) + jw.UInt64Field("rangeMinEpoch", gs.RangeMinEpoch) + jw.UInt64Field("rangeMaxEpoch", gs.RangeMaxEpoch) jw.EndObject() } diff --git a/repo/maintenancestats/stats_rewrite_contents.go b/repo/maintenancestats/stats_rewrite_contents.go index f86681b6c86..def0e576432 100644 --- a/repo/maintenancestats/stats_rewrite_contents.go +++ b/repo/maintenancestats/stats_rewrite_contents.go @@ -4,36 +4,37 @@ import ( "fmt" "github.com/kopia/kopia/internal/contentlog" + "github.com/kopia/kopia/internal/units" ) const rewriteContentsStatsKind = "rewriteContentsStats" // RewriteContentsStats are the stats for rewriting contents. type RewriteContentsStats struct { - ToRewriteContentCount int `json:"toRewriteContentCount"` - ToRewriteContentSize int64 `json:"toRewriteContentSize"` - RewrittenContentCount int `json:"rewrittenContentCount"` - RewrittenContentSize int64 `json:"rewrittenContentSize"` - RetainedContentCount int `json:"retainedContentCount"` - RetainedContentSize int64 `json:"retainedContentSize"` + ToRewriteContentCount uint64 `json:"toRewriteContentCount"` + ToRewriteContentSize uint64 `json:"toRewriteContentSize"` + RewrittenContentCount uint64 `json:"rewrittenContentCount"` + RewrittenContentSize uint64 `json:"rewrittenContentSize"` + RetainedContentCount uint64 `json:"retainedContentCount"` + RetainedContentSize uint64 `json:"retainedContentSize"` } // WriteValueTo writes the stats to JSONWriter. func (rs *RewriteContentsStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(rs.Kind()) - jw.IntField("toRewriteContentCount", rs.ToRewriteContentCount) - jw.Int64Field("toRewriteContentSize", rs.ToRewriteContentSize) - jw.IntField("rewrittenContentCount", rs.RewrittenContentCount) - jw.Int64Field("rewrittenContentSize", rs.RewrittenContentSize) - jw.IntField("retainedContentCount", rs.RetainedContentCount) - jw.Int64Field("retainedContentSize", rs.RetainedContentSize) + jw.UInt64Field("toRewriteContentCount", rs.ToRewriteContentCount) + jw.UInt64Field("toRewriteContentSize", rs.ToRewriteContentSize) + jw.UInt64Field("rewrittenContentCount", rs.RewrittenContentCount) + jw.UInt64Field("rewrittenContentSize", rs.RewrittenContentSize) + jw.UInt64Field("retainedContentCount", rs.RetainedContentCount) + jw.UInt64Field("retainedContentSize", rs.RetainedContentSize) jw.EndObject() } // Summary generates a human readable summary for the stats. func (rs *RewriteContentsStats) Summary() string { return fmt.Sprintf("Found %v(%v) contents to rewrite and rewrote %v(%v). Retained %v(%v) contents from rewrite", - rs.ToRewriteContentCount, rs.ToRewriteContentSize, rs.RewrittenContentCount, rs.RewrittenContentSize, rs.RetainedContentCount, rs.RetainedContentSize) + rs.ToRewriteContentCount, units.BytesString(rs.ToRewriteContentSize), rs.RewrittenContentCount, units.BytesString(rs.RewrittenContentSize), rs.RetainedContentCount, units.BytesString(rs.RetainedContentSize)) } // Kind returns the kind name for the stats. diff --git a/repo/maintenancestats/stats_snapshot_gc.go b/repo/maintenancestats/stats_snapshot_gc.go index 47ccb169442..02de50653fd 100644 --- a/repo/maintenancestats/stats_snapshot_gc.go +++ b/repo/maintenancestats/stats_snapshot_gc.go @@ -10,35 +10,35 @@ const snapshotGCStatsKind = "snapshotGCStats" // SnapshotGCStats delivers are the stats for snapshot GC. type SnapshotGCStats struct { - UnreferencedContentCount uint32 `json:"unreferencedContentCount"` - UnreferencedContentSize int64 `json:"unreferencedContentSize"` - DeletedContentCount uint32 `json:"deletedContentCount"` - DeletedContentSize int64 `json:"deletedContentSize"` - UnreferencedRecentContentCount uint32 `json:"unreferencedRecentContentCount"` - UnreferencedRecentContentSize int64 `json:"unreferencedRecentContentSize"` - InUseContentCount uint32 `json:"inUseContentCount"` - InUseContentSize int64 `json:"inUseContentSize"` - InUseSystemContentCount uint32 `json:"inUseSystemContentCount"` - InUseSystemContentSize int64 `json:"inUseSystemContentSize"` - RecoveredContentCount uint32 `json:"recoveredContentCount"` - RecoveredContentSize int64 `json:"recoveredContentSize"` + UnreferencedContentCount uint64 `json:"unreferencedContentCount"` + UnreferencedContentSize uint64 `json:"unreferencedContentSize"` + DeletedContentCount uint64 `json:"deletedContentCount"` + DeletedContentSize uint64 `json:"deletedContentSize"` + UnreferencedRecentContentCount uint64 `json:"unreferencedRecentContentCount"` + UnreferencedRecentContentSize uint64 `json:"unreferencedRecentContentSize"` + InUseContentCount uint64 `json:"inUseContentCount"` + InUseContentSize uint64 `json:"inUseContentSize"` + InUseSystemContentCount uint64 `json:"inUseSystemContentCount"` + InUseSystemContentSize uint64 `json:"inUseSystemContentSize"` + RecoveredContentCount uint64 `json:"recoveredContentCount"` + RecoveredContentSize uint64 `json:"recoveredContentSize"` } // WriteValueTo writes the stats to JSONWriter. func (ss *SnapshotGCStats) WriteValueTo(jw *contentlog.JSONWriter) { jw.BeginObjectField(ss.Kind()) - jw.UInt32Field("unreferencedContentCount", ss.UnreferencedContentCount) - jw.Int64Field("unreferencedContentSize", ss.UnreferencedContentSize) - jw.UInt32Field("deletedContentCount", ss.DeletedContentCount) - jw.Int64Field("deletedContentSize", ss.DeletedContentSize) - jw.UInt32Field("unreferencedRecentContentCount", ss.UnreferencedRecentContentCount) - jw.Int64Field("unreferencedRecentContentSize", ss.UnreferencedRecentContentSize) - jw.UInt32Field("inUseContentCount", ss.InUseContentCount) - jw.Int64Field("inUseContentSize", ss.InUseContentSize) - jw.UInt32Field("inUseSystemContentCount", ss.InUseSystemContentCount) - jw.Int64Field("inUseSystemContentSize", ss.InUseSystemContentSize) - jw.UInt32Field("recoveredContentCount", ss.RecoveredContentCount) - jw.Int64Field("recoveredContentSize", ss.RecoveredContentSize) + jw.UInt64Field("unreferencedContentCount", ss.UnreferencedContentCount) + jw.UInt64Field("unreferencedContentSize", ss.UnreferencedContentSize) + jw.UInt64Field("deletedContentCount", ss.DeletedContentCount) + jw.UInt64Field("deletedContentSize", ss.DeletedContentSize) + jw.UInt64Field("unreferencedRecentContentCount", ss.UnreferencedRecentContentCount) + jw.UInt64Field("unreferencedRecentContentSize", ss.UnreferencedRecentContentSize) + jw.UInt64Field("inUseContentCount", ss.InUseContentCount) + jw.UInt64Field("inUseContentSize", ss.InUseContentSize) + jw.UInt64Field("inUseSystemContentCount", ss.InUseSystemContentCount) + jw.UInt64Field("inUseSystemContentSize", ss.InUseSystemContentSize) + jw.UInt64Field("recoveredContentCount", ss.RecoveredContentCount) + jw.UInt64Field("recoveredContentSize", ss.RecoveredContentSize) jw.EndObject() } diff --git a/repo/maintenancestats/typeconversion.go b/repo/maintenancestats/typeconversion.go new file mode 100644 index 00000000000..1cb5b279708 --- /dev/null +++ b/repo/maintenancestats/typeconversion.go @@ -0,0 +1,23 @@ +package maintenancestats + +import ( + "log" + + "golang.org/x/time/rate" +) + +var negativeValueWarningLimit = rate.Sometimes{First: 10} //nolint:mnd + +// ToUint64 converts v from a signed integer type T to uint64 while checking that +// the value is non-negative. It returns 0 for negative values. +func ToUint64[T int8 | int16 | int32 | int | int64](v T) uint64 { + if v >= 0 { + return uint64(v) + } + + negativeValueWarningLimit.Do(func() { + log.Println("warning, converting negative value to uint64:", v) + }) + + return 0 +} diff --git a/repo/maintenancestats/typeconversion_test.go b/repo/maintenancestats/typeconversion_test.go new file mode 100644 index 00000000000..283a1936bd7 --- /dev/null +++ b/repo/maintenancestats/typeconversion_test.go @@ -0,0 +1,41 @@ +package maintenancestats + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestToUint64(t *testing.T) { + cases := []struct { + in int + expected uint64 + }{ + { + in: math.MinInt, + expected: 0, + }, + { + in: -1, + expected: 0, + }, + { + in: 0, + expected: 0, + }, + { + in: 1, + expected: 1, + }, + { + in: math.MaxInt, + expected: math.MaxInt, + }, + } + + for _, c := range cases { + v := ToUint64(c.in) + require.Equal(t, c.expected, v) + } +} diff --git a/repo/object/object_manager.go b/repo/object/object_manager.go index d04b4acb3b6..4f4ec2121ad 100644 --- a/repo/object/object_manager.go +++ b/repo/object/object_manager.go @@ -129,7 +129,7 @@ func (om *Manager) Concatenate(ctx context.Context, objectIDs []ID, metadataComp } } - log(ctx).Debugf("concatenated: %v total: %v", concatenatedEntries, totalLength) + log(ctx).Debugf("concatenated %d entries, total object length: %d", len(concatenatedEntries), totalLength) w := om.NewWriter(ctx, WriterOptions{ Prefix: indirectContentPrefix, diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go index 4913b315dbf..61479ed8848 100644 --- a/repo/object/object_manager_test.go +++ b/repo/object/object_manager_test.go @@ -802,6 +802,10 @@ func TestEndToEndReadAndSeekWithCompression(t *testing.T) { for _, compressible := range []bool{false, true} { for compressorName := range compression.ByName { + if !compression.IsSupported(compressorName) { + continue + } + t.Run(string(compressorName), func(t *testing.T) { ctx := testlogging.Context(t) diff --git a/site/.gitignore b/site/.gitignore index eb832e0e7a8..f535871bb24 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -3,6 +3,5 @@ resources/ node_modules/ tech-doc-hugo .tools/ -package-lock.json *.deb .hugo_build.lock diff --git a/site/content/_index.html b/site/content/_index.html index b1edb381d07..873787c5269 100644 --- a/site/content/_index.html +++ b/site/content/_index.html @@ -13,13 +13,10 @@ Download -
- User Support Forum - - - Developer Discussion + User Forum +

Encrypted, Compressed, and Deduplicated Backups Using the Cloud Storage You Pick.

Supports GUI and CLI on Windows, macOS and Linux.

@@ -52,14 +49,11 @@

Demo of Kopia's Graphical User Interface

With a secure and scalable architecture, **Kopia** can back up everything from small laptops to large servers. {{% /blocks/feature %}} - -{{% blocks/feature icon="fab fa-github" title="Contributions Welcome!" url="https://github.com/kopia/kopia/blob/master/GOVERNANCE.md" %}} -We use a [Pull Request](https://github.com/kopia/kopia/pulls) contributions workflow on GitHub. New contributors and bug reports are always welcome! +{{% blocks/feature icon="fas fa-comments" title="Join The Conversation" %}} +Join the [user forum](https://kopia.discourse.group) to get started with using **Kopia**, discuss features and issues. {{% /blocks/feature %}} - -{{% blocks/feature icon="fab fa-slack" title="Join The Conversation" %}} -Find us on [Slack](https://slack.kopia.io) to get started with using **Kopia**, discuss features and issues, meet the team, and more. +{{% blocks/feature icon="fab fa-github" title="Contributing" url="https://github.com/kopia/kopia/blob/master/GOVERNANCE.md" %}} {{% /blocks/feature %}} diff --git a/site/content/docs/Advanced/Actions/_index.md b/site/content/docs/Advanced/Actions/_index.md index e1d580f9bba..80cdb227b29 100644 --- a/site/content/docs/Advanced/Actions/_index.md +++ b/site/content/docs/Advanced/Actions/_index.md @@ -253,7 +253,7 @@ kopia policy set --after-folder-action "powershell -WindowStyle Hi Those are just some initial ideas, we're certain more interesting types of actions will be developed using this mechanism, including LVM snapshots, BTRFS Snapshots, notifications and more. -If you have ideas for extending this mechanism, definitely [file an Issue on Github](https://github.com/kopia/kopia/issues). +If you have ideas for extending this mechanism, please [file an Issue on Github](https://github.com/kopia/kopia/issues). If you develop a useful action script that you'd like to share with the community, we encourage you to do so by sending us a pull request to add to this web page or you can put them in your own repository and we'll be happy to link it from here. diff --git a/site/content/docs/Advanced/Architecture/_index.md b/site/content/docs/Advanced/Architecture/_index.md index 5fad066a525..f1deeb9f595 100644 --- a/site/content/docs/Advanced/Architecture/_index.md +++ b/site/content/docs/Advanced/Architecture/_index.md @@ -16,7 +16,7 @@ The following diagram illustrates the key components of Kopia: ### Binary Large Object Storage (BLOB) -BLOB storage is the place where your data is ultimately stored. Any type that implements simple Go [API](https://godoc.org/github.com/kopia/kopia/repo/blob#Storage) can be used as Kopia's blob storage. +BLOB storage is the place where your data is ultimately stored. Any type that implements a simple Go [API](https://godoc.org/github.com/kopia/kopia/repo/blob#Storage) can be used as Kopia's blob storage. See the [Repositories](/docs/repositories/) page for a list of currently supported storage backends. @@ -34,11 +34,11 @@ Content-Addressable Block Storage manages data blocks of relatively small sizes Block ID is generated by applying [cryptographic hash function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) such as SHA2 or BLAKE2S to produce a pseudo-random identifier, such as `6a9fc3a464a79360269e20b88cef629a`. -A key property of block identifiers is that two identical block of data will produce exactly the same identifiers, thus resulting in natural de-duplication of data. +A key property of block identifiers is that two identical blocks of data will produce exactly the same identifiers, thus resulting in natural de-duplication of data. -After hashing, the block data is encrypted using algorithm such as `AES256-GCM-HMAC-SHA256` or `CHACHA20-POLY1305-HMAC-SHA256`. To make uploads to cloud storage more efficient and cheaper, multiple smaller blocks are combined into larger **Packs** of 20-40MB each. Pack files in blob storage have random names and don't reveal anything about their contents or structure. Their sizes are also generally unrelated to content due to splitting and merging. +After hashing, the block data is encrypted using algorithm such as `AES256-GCM-HMAC-SHA256` or `CHACHA20-POLY1305-HMAC-SHA256`. To make uploads to cloud storage more efficient and cheaper, multiple smaller blocks are combined into larger **Packs** of 20-40MB each. Pack files in blob storage have random names and don't reveal anything about their contents or structure. Their sizes are also generally unrelated to their content due to splitting and merging. -To help efficiently find a block in the blob storage, CABS maintains an index that maps block ID to the blob file name, offset within the file and length. In addition, to make data recovery possible in case the index files got corrupted, a local index is also stored at the end of each pack. +To help efficiently find a block in the blob storage, CABS maintains an index that maps a block ID to the blob file name, offset within the file and length. In addition, to make data recovery possible in case the index files are corrupted, a local index is also stored at the end of each pack. File names in the BLOB layer have prefixes to help quickly identify its type: @@ -46,9 +46,9 @@ File names in the BLOB layer have prefixes to help quickly identify its type: * `q` represents packs containing metadata (e.g. `q7a9939814e8aba1fdda2d87965f324d3`) * `x` represents indices (e.g. `xn0_20db7984bd71c4042cea471a61fbcea1`) -CABS is not meant to be used directly, instead it's a building block for object storage (CAOS) and manifest storage layers (LAMS) described below. +CABS is not meant to be used directly, instead it's a building block for the object storage (CAOS) and manifest storage layers (LAMS) described below. -The API for CABS can be found in https://godoc.org/github.com/kopia/kopia/repo/content +The API for CABS can be found at https://godoc.org/github.com/kopia/kopia/repo/content ### Content-Addressable Object Storage (CAOS) @@ -64,7 +64,7 @@ Object IDs can also have an optional single-letter prefix `g..z` that helps quic Objects without prefixes are stored in the `p` pack while objects with prefixes are stored in the `q` metadata pack by the CABS layer. -To represent objects larger than the size of a single CABS block, Kopia links together multiple blocks via special indirect JSON content. Such blocks are distinguished from regular blocks the `I` virtual prefix. Indirection can be applied to any object type, including metadata such as directory listing. However, when applied to a data block, an `x` prefix is added to ensure than the indirect JSON is stored in a metadata pack instead of a data pack by the CABS layer. +To represent objects larger than the size of a single CABS block, Kopia links together multiple blocks via special indirect JSON content. Such blocks are distinguished from regular blocks by the `I` virtual prefix. Indirection can be applied to any object type, including metadata such as directory listing. However, when applied to a data block, an `x` prefix is added to ensure than the indirect JSON is stored in a metadata pack instead of a data pack by the CABS layer. For example large file object might have an identifier such as `Ixac47f7ce280fdd81f04c670fec2353dc` and JSON content: @@ -96,14 +96,14 @@ $ kopia content show Ixac47f7ce280fdd81f04c670fec2353dc Note that the example above shows that `I` is a virtual prefix. The actual CABS block does not content the `I` prefix but referencing the block with the `I` prefix tells Kopia to interpret the block as an indirect JSON object and return the resolved content instead of the raw content. -The API for CAOS can be found in https://godoc.org/github.com/kopia/kopia/repo/object +The API for CAOS can be found at https://godoc.org/github.com/kopia/kopia/repo/object ### Label-Addressable Manifest Storage (LAMS) -While content-addressable storage is a neat idea, dealing with cryptographic hashes is not very convenient for humans to use. +While content-addressable storage is a neat idea, dealing with cryptographic hashes is not very convenient for humans. To address that, Kopia supports another type of storage, used to persist small JSON objects called **Manifests** (describing snapshots, policies, etc.) which are identified by arbitrary `key=value` pairs called labels. Internally manifests are stored as CABS blocks. -The API for LAMS can be found in https://godoc.org/github.com/kopia/kopia/repo/manifest +The API for LAMS can be found at https://godoc.org/github.com/kopia/kopia/repo/manifest diff --git a/site/content/docs/Advanced/Compression/_index.md b/site/content/docs/Advanced/Compression/_index.md index 68910f7ca87..d3f998aae57 100644 --- a/site/content/docs/Advanced/Compression/_index.md +++ b/site/content/docs/Advanced/Compression/_index.md @@ -35,18 +35,17 @@ Repeating 1 times per compression method (total 466.7 MiB). 3. s2-parallel-4 127.1 MiB 2.3 GiB/s 2951 344.1 MiB 4. pgzip-best-speed 96.7 MiB 2.1 GiB/s 4127 324.1 MiB 5. pgzip 86.3 MiB 1.2 GiB/s 4132 298.7 MiB - 6. lz4 131.8 MiB 458.9 MiB/s 17 321.7 MiB - 7. zstd-fastest 79.8 MiB 356.2 MiB/s 22503 246 MiB - 8. zstd 76.8 MiB 323.7 MiB/s 22605 237.8 MiB - 9. deflate-best-speed 96.7 MiB 220.8 MiB/s 45 310.8 MiB - 10. gzip-best-speed 94.9 MiB 165 MiB/s 40 305.2 MiB - 11. deflate-default 86.3 MiB 143.1 MiB/s 34 311 MiB - 12. zstd-better-compression 74.2 MiB 104 MiB/s 22496 251.4 MiB - 13. pgzip-best-compression 83 MiB 55.9 MiB/s 4359 299.1 MiB - 14. gzip 83.6 MiB 40.5 MiB/s 69 304.8 MiB - 15. zstd-best-compression 68.9 MiB 19.2 MiB/s 22669 303.4 MiB - 16. deflate-best-compression 83 MiB 5.6 MiB/s 134 311 MiB - 17. gzip-best-compression 83 MiB 5.1 MiB/s 137 304.8 MiB + 6. zstd-fastest 79.8 MiB 356.2 MiB/s 22503 246 MiB + 7. zstd 76.8 MiB 323.7 MiB/s 22605 237.8 MiB + 8. deflate-best-speed 96.7 MiB 220.8 MiB/s 45 310.8 MiB + 9. gzip-best-speed 94.9 MiB 165 MiB/s 40 305.2 MiB + 10. deflate-default 86.3 MiB 143.1 MiB/s 34 311 MiB + 11. zstd-better-compression 74.2 MiB 104 MiB/s 22496 251.4 MiB + 12. pgzip-best-compression 83 MiB 55.9 MiB/s 4359 299.1 MiB + 13. gzip 83.6 MiB 40.5 MiB/s 69 304.8 MiB + 14. zstd-best-compression 68.9 MiB 19.2 MiB/s 22669 303.4 MiB + 15. deflate-best-compression 83 MiB 5.6 MiB/s 134 311 MiB + 16. gzip-best-compression 83 MiB 5.1 MiB/s 137 304.8 MiB ``` As you can see, s2 compression clearly favors performance over compression ratio. zstd on the other hand results almost half the size as s2. pgzip arguably offers the best balance on two worlds. @@ -73,21 +72,23 @@ Repeating 100 times per compression method (total 12.5 MiB). 8. gzip-best-speed 33.7 KiB 150.6 MiB/s 28 1.2 MiB 9. pgzip-best-speed 34.3 KiB 143.7 MiB/s 1649 220.2 MiB 10. deflate-default 31.2 KiB 126.3 MiB/s 22 1.1 MiB - 11. lz4 44.7 KiB 112.6 MiB/s 435 816.7 MiB - 12. pgzip 31.2 KiB 94.6 MiB/s 2634 277.5 MiB - 13. gzip 30.4 KiB 39.5 MiB/s 26 874.7 KiB - 14. deflate-best-compression 30.4 KiB 25.4 MiB/s 21 1 MiB - 15. gzip-best-compression 30.4 KiB 24.5 MiB/s 27 874.9 KiB - 16. pgzip-best-compression 30.4 KiB 23.1 MiB/s 2646 281.8 MiB - 17. zstd-best-compression 25.1 KiB 19.3 MiB/s 882 99.3 MiB + 11. pgzip 31.2 KiB 94.6 MiB/s 2634 277.5 MiB + 12. gzip 30.4 KiB 39.5 MiB/s 26 874.7 KiB + 13. deflate-best-compression 30.4 KiB 25.4 MiB/s 21 1 MiB + 14. gzip-best-compression 30.4 KiB 24.5 MiB/s 27 874.9 KiB + 15. pgzip-best-compression 30.4 KiB 23.1 MiB/s 2646 281.8 MiB + 16. zstd-best-compression 25.1 KiB 19.3 MiB/s 882 99.3 MiB ``` -While s2 significantly uses way less memory in this case, pgzip's numbers seem indifferent to the input size. [Turns out](https://github.com/klauspost/pgzip/issues/44), pgzip has different memory usage logic. It would quickly allocate necessary memory and plateau, which s2 is more on a linear fashion. Here is rough graph is demonstrate the difference: +While s2 uses significantly less memory in this case, pgzip's numbers seem indifferent to the input size. [Turns out](https://github.com/klauspost/pgzip/issues/44), pgzip has different memory usage logic. It would quickly allocate necessary memory and plateau, which s2 exhibits more of a linear growth. The graph below illustrates the difference. ![s2 vs pgzip](s2_vs_pgzip.svg) Therefore, if your backup target is small, and memory is extremely restricted, s2 might be necessary. Otherwise, all algorithms are valid candidates. +Note: Newer Kopia versions no longer support reading contents that were compressed with the deprecated LZ4 algorithm. If your repository contains data written with LZ4, you must migrate it first using a Kopia version that still supports LZ4—for example by restoring the affected snapshots and/or repacking the repository with one of the currently supported compression algorithms—before upgrading. + + ### Minimum file size and extensions to compress As discussed above, some compression algorithms make sense only if the payload is large enough. So it might be beneficial to set a minimum file size when using these algorithms. diff --git a/site/content/docs/Advanced/Consistency/_index.md b/site/content/docs/Advanced/Consistency/_index.md index 8e8c547fd56..60c6a475010 100644 --- a/site/content/docs/Advanced/Consistency/_index.md +++ b/site/content/docs/Advanced/Consistency/_index.md @@ -52,6 +52,6 @@ There are few tips to try, which are generally safe to try: 4. If a repository can’t be opened but was working recently and maintenance has not run yet, it may be helpful to try to remove (or stash away) the most recently-written index files whose names start with `x` in the reverse timestamp order one by one until the issue is fixed. This will effectively roll back the repository writes to a prior state. Exercise caution when removing the files. -5. If the steps above do not help, report your issue on https://kopia.discourse.group or https://slack.kopia.io. Kopia has many low-level data recovery tools, but they should not be used by end users without guidance from developers. +5. If the steps above do not help, report your issue on https://kopia.discourse.group. -> NOTE: Since all data corruption cases are unique, it’s generally not recommended to attempt fixes recommended to other users even for possibly similar issues, since the particular fix method may not be applicable. +> NOTE: While Kopia has many low-level data recovery tools, it’s generally not recommended to attempt fixes recommended to other users even for possibly similar issues, since all data corruption cases are unique and the particular fix method may not be applicable. Following those methods may damage the repository further and make the data completely unrecoverable. diff --git a/site/content/docs/Advanced/Maintenance/_index.md b/site/content/docs/Advanced/Maintenance/_index.md index b5edbb58de3..425c67f593a 100644 --- a/site/content/docs/Advanced/Maintenance/_index.md +++ b/site/content/docs/Advanced/Maintenance/_index.md @@ -6,36 +6,36 @@ weight: 30 ## Maintenance -Kopia repositories require periodic maintenance to ensure best possible performance and optimal storage usage. +Kopia repositories require periodic maintenance to ensure the best possible performance and optimal storage usage. -Starting with v0.6.0 the repository maintenance is automatic and will happen occasionally when `kopia` command-line client is used. This document describes maintenance functionality in greater detail. +Starting with v0.6.0, the repository maintenance is automatic, and will happen occasionally when the `kopia` command-line client is used. This document describes the maintenance functionality in greater detail. ### Maintenance Tasks Kopia uses the following types of maintenance tasks: -* **Quick Maintenance Tasks** are primarily responsible for keeping the number of frequently accessed blobs (`q` and `n`) low to ensure good performance. +* **Quick Maintenance Tasks** are primarily responsible for keeping the number of frequently-accessed blobs (`q` and `n`) low to ensure good performance. Quick Maintenance will never delete any metadata from the repository without ensuring that another copy of the same metadata exists. Quick Maintenance Tasks are enabled by default and will execute approximately every hour. - While the user can disable quick maintenance, it's not recommended as it will lead to reduced performance. + While the user can disable quick maintenance, it's not recommended, as it will lead to reduced performance. * **Full Maintenance Tasks** are responsible for keeping the repository compact and eliminate deleted files that the user no longer wishes to store. - The most important task is Snapshot GC which marks for deletion all contents that are no longer reachable from any of the active snapshots. Full Maintenance is also responsible for compaction of data pack blobs (`p`) after contents stored in them have been deleted. Full Maintenance Tasks are enabled by default and will execute every 24 hours. + The most important task is Snapshot GC, which marks for deletion all contents that are no longer reachable from any of the active snapshots. Full Maintenance is also responsible for compaction of data pack blobs (`p`) after contents stored in them have been deleted. Full Maintenance Tasks are enabled by default and will execute every 24 hours. ### Maintenance Task Ownership For correctness reasons, Kopia requires that no more than one instance of certain maintenance operations runs at any given time. To achieve that, one repository `user@hostname` is designated as the Maintenance Owner. Other repository users will not attempt to run maintenance automatically and the designated user will attempt to do so after holding an exclusive lock. -To see the current maintenance owner use `kopia maintenance info` command: +To see the current maintenance owner, use the `kopia maintenance info` command: ``` $ kopia maintenance info Owner: root@myhost ``` -To change the maintenance owner to either current user or another user use `kopia maintenance set` command: +To change the maintenance owner to either current user or another user, use the `kopia maintenance set` command: ``` $ kopia maintenance set --owner=me @@ -58,7 +58,7 @@ $ kopia maintenance set --quick-interval=2h $ kopia maintenance set --full-interval=8h ``` -It is also possible to pause quick or full maintenance for some time so that it automatically resumes after specified time elapses. To change the quick or full maintenance for some time use: +It is also possible to pause quick or full maintenance for some time so that it automatically resumes after the specified time elapses. To change the quick or full maintenance for a period of time, use: ``` $ kopia maintenance set --pause-quick=48h @@ -67,7 +67,7 @@ $ kopia maintenance set --pause-full=268h ### Manually Running Maintenance -To run maintenance manually use `kopia maintenance run`: +To run maintenance manually, use the `kopia maintenance run` command: ``` # quick maintenance @@ -81,11 +81,11 @@ The current user must be the maintenance owner. ### Maintenance Safety -Kopia's maintenance routine follows certain safety rules which rely on passage of time to ensure correctness. This is needed in case other Kopia clients are currently operating on the repository. To guarantee correctness, certain length of time must pass to ensure all caches and transient state are properly synchronized with the repository. Kopia must also account for eventual consistency delays introduced by the blob storage provider. +Kopia's maintenance routine follows certain safety rules which rely on the passage of time to ensure correctness. This is needed in case other Kopia clients are currently operating on the repository. To guarantee correctness, certain length of time must pass to ensure all caches and transient state are properly synchronized with the repository. Kopia must also account for eventual consistency delays introduced by the blob storage provider. -This means that effects of full maintenance are not immediate - it may take several hours and/or multiple maintenance cycles to remove blobs that are not in use. +This means that the effects of full maintenance are not immediate - it may take several hours and/or multiple maintenance cycles to remove blobs that are not in use. -Kopia 0.8 adds new flag that can be used to speed up full maintenance if the user can guarantee no kopia snapshots are being created. +Kopia 0.8 adds a new flag that can be used to speed up full maintenance if the user can guarantee no Kopia snapshots are being created. >WARNING: As the name implies, the `--safety=none` flag disables all safety features, so the user must ensure that no concurrent operations are happening and repository storage is properly in sync before attempting it. Failure to do so can introduce repository corruption. @@ -97,5 +97,5 @@ $ kopia maintenance run --full --safety=none ### Viewing Maintenance History -To view the history of maintenance operations use `kopia maintenance info`, which will display the history of last 5 maintenance runs. +To view the history of maintenance operations, use the `kopia maintenance info` command, which will display the history of the last 5 maintenance runs. diff --git a/site/content/docs/Features/_index.md b/site/content/docs/Features/_index.md index 97c5412863d..67ea20c5b83 100644 --- a/site/content/docs/Features/_index.md +++ b/site/content/docs/Features/_index.md @@ -125,7 +125,7 @@ Kopia supports [Reed-Solomon error correction algorithm](../advanced/ecc/) to he ### Verifying Backup Validity and Consistency -Backing up data is great, but you also need to be able to restore that data when (if) the time arises. Kopia has built-in functions that enable you to verify the consistency/validity of your backed up files. You can run these consistency checks are frequently as you like (e.g., once a month, once a year, etc.). Read the [repository consistency](../advanced/consistency/) help docs for more information. +Backing up data is great, but you also need to be able to restore that data when (if) the time arises. Kopia has built-in functions that enable you to verify the consistency/validity of your backed up files. You can run these consistency checks as frequently as you like (e.g., once a month, once a year, etc.). Read the [repository consistency](../advanced/consistency/) help docs for more information. ### Recovering Backed Up Data When There is Data Loss diff --git a/site/content/docs/Mounting/_index.md b/site/content/docs/Mounting/_index.md index 7f43046a3f1..8b393978b37 100644 --- a/site/content/docs/Mounting/_index.md +++ b/site/content/docs/Mounting/_index.md @@ -106,3 +106,15 @@ PS> kopia mount all Z: # mount successful Mounted 'all' on Z: Press Ctrl-C to unmount. ``` + +## macOS + +On macOS, the recommended option is `kopia mount --webdav` for a WebDAV-based mount. This does not require any +additional software to be installed. + +As an alternative, FUSE-based mounts with `kopia mount` can be achieved using [macFUSE](https://macfuse.github.io/) and +its kernel backend. Refer to the [macFUSE getting started guide](https://github.com/macfuse/macfuse/wiki/Getting-Started#kernel-backend) for the current installation requirements +for your macOS version and architecture. + +>NOTE: KopiaUI "Mount as Local Filesystem" button currently uses FUSE-based mount paths on macOS, thus it requires +>macFUSE to be installed. diff --git a/site/go.mod b/site/go.mod index ff7a94a609d..fab555c0ebe 100644 --- a/site/go.mod +++ b/site/go.mod @@ -1,8 +1,3 @@ module github.com/kopia/kopia/site go 1.25 - -require ( - github.com/google/docsy v0.7.0 // indirect - github.com/google/docsy/dependencies v0.7.0 // indirect -) diff --git a/site/go.sum b/site/go.sum deleted file mode 100644 index 7e7d9becd85..00000000000 --- a/site/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= -github.com/google/docsy v0.7.0 h1:JaeZ0/KufX/BJ3SyATb/fmZa1DFI7o5d9KU+i6+lLJY= -github.com/google/docsy v0.7.0/go.mod h1:5WhIFchr5BfH6agjcInhpLRz7U7map0bcmKSpcrg6BE= -github.com/google/docsy/dependencies v0.7.0 h1:/xUlWCZOSMDubHfrhIz1YtaRn2Oc/swfJ7OUfglXE8U= -github.com/google/docsy/dependencies v0.7.0/go.mod h1:gihhs5gmgeO+wuoay4FwOzob+jYJVyQbNaQOh788lD4= -github.com/twbs/bootstrap v5.2.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= diff --git a/site/hugo.toml b/site/hugo.toml index 187aaeeb5ee..15932a09505 100644 --- a/site/hugo.toml +++ b/site/hugo.toml @@ -117,16 +117,11 @@ no = 'Sorry to hear that. Please = 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-cli": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz", + "integrity": "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.3.0", + "dependency-graph": "^0.11.0", + "fs-extra": "^11.0.0", + "get-stdin": "^9.0.0", + "globby": "^13.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^4.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "postcss": "index.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-reporter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz", + "integrity": "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/site/package.json b/site/package.json index 2d369e13318..dbfb9d3efac 100644 --- a/site/package.json +++ b/site/package.json @@ -7,7 +7,7 @@ "license": "ISC", "devDependencies": { "autoprefixer": "^10.4.14", - "postcss": "^8.4.24", + "postcss": "^8.5.12", "postcss-cli": "^10.1.0" } } diff --git a/snapshot/restore/local_fs_output.go b/snapshot/restore/local_fs_output.go index 1d43ba03b0a..43cc026e787 100644 --- a/snapshot/restore/local_fs_output.go +++ b/snapshot/restore/local_fs_output.go @@ -15,6 +15,7 @@ import ( "github.com/kopia/kopia/fs/localfs" "github.com/kopia/kopia/internal/atomicfile" "github.com/kopia/kopia/internal/iocopy" + "github.com/kopia/kopia/internal/ospath" "github.com/kopia/kopia/internal/sparsefile" "github.com/kopia/kopia/internal/stat" "github.com/kopia/kopia/snapshot" @@ -433,7 +434,7 @@ func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath strin } log(ctx).Debugf("copying file contents to: %v", targetPath) - targetPath = atomicfile.MaybePrefixLongFilenameOnWindows(targetPath) + targetPath = ospath.SafeLongFilename(targetPath) if o.WriteFilesAtomically { //nolint:wrapcheck diff --git a/snapshot/restore/local_fs_output_windows.go b/snapshot/restore/local_fs_output_windows.go index 1825ac8640b..a254763e871 100644 --- a/snapshot/restore/local_fs_output_windows.go +++ b/snapshot/restore/local_fs_output_windows.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" "golang.org/x/sys/windows" - "github.com/kopia/kopia/internal/atomicfile" + "github.com/kopia/kopia/internal/ospath" ) //nolint:revive @@ -24,7 +24,7 @@ func symlinkChtimes(linkPath string, atime, mtime time.Time) error { fta := windows.NsecToFiletime(atime.UnixNano()) ftw := windows.NsecToFiletime(mtime.UnixNano()) - linkPath = atomicfile.MaybePrefixLongFilenameOnWindows(linkPath) + linkPath = ospath.SafeLongFilename(linkPath) fn, err := windows.UTF16PtrFromString(linkPath) if err != nil { diff --git a/snapshot/restore/shallow_helper.go b/snapshot/restore/shallow_helper.go index 24dc9df2065..48ba6440161 100644 --- a/snapshot/restore/shallow_helper.go +++ b/snapshot/restore/shallow_helper.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/kopia/kopia/fs/localfs" - "github.com/kopia/kopia/internal/atomicfile" + "github.com/kopia/kopia/internal/ospath" ) // PathIfPlaceholder returns the placeholder suffix trimmed from path or the @@ -23,7 +23,7 @@ func PathIfPlaceholder(path string) string { func SafeRemoveAll(path string) error { if SafelySuffixablePath(path) { //nolint:wrapcheck - return os.RemoveAll(atomicfile.MaybePrefixLongFilenameOnWindows(path + localfs.ShallowEntrySuffix)) + return os.RemoveAll(ospath.SafeLongFilename(path + localfs.ShallowEntrySuffix)) } // path can't possibly exist because we could have never written a file diff --git a/snapshot/snapshotgc/gc.go b/snapshot/snapshotgc/gc.go index 8c97dc2cb00..53c4901ad63 100644 --- a/snapshot/snapshotgc/gc.go +++ b/snapshot/snapshotgc/gc.go @@ -214,28 +214,28 @@ func buildGCResult(unused, inUse, system, tooRecent, undeleted, deleted *stats.C result := &maintenancestats.SnapshotGCStats{} cnt, size := unused.Approximate() - result.UnreferencedContentCount = cnt - result.UnreferencedContentSize = size + result.UnreferencedContentCount = uint64(cnt) + result.UnreferencedContentSize = maintenancestats.ToUint64(size) cnt, size = tooRecent.Approximate() - result.UnreferencedRecentContentCount = cnt - result.UnreferencedRecentContentSize = size + result.UnreferencedRecentContentCount = uint64(cnt) + result.UnreferencedRecentContentSize = maintenancestats.ToUint64(size) cnt, size = inUse.Approximate() - result.InUseContentCount = cnt - result.InUseContentSize = size + result.InUseContentCount = uint64(cnt) + result.InUseContentSize = maintenancestats.ToUint64(size) cnt, size = system.Approximate() - result.InUseSystemContentCount = cnt - result.InUseSystemContentSize = size + result.InUseSystemContentCount = uint64(cnt) + result.InUseSystemContentSize = maintenancestats.ToUint64(size) cnt, size = undeleted.Approximate() - result.RecoveredContentCount = cnt - result.RecoveredContentSize = size + result.RecoveredContentCount = uint64(cnt) + result.RecoveredContentSize = maintenancestats.ToUint64(size) cnt, size = deleted.Approximate() - result.DeletedContentCount = cnt - result.DeletedContentSize = size + result.DeletedContentCount = uint64(cnt) + result.DeletedContentSize = maintenancestats.ToUint64(size) return result } diff --git a/snapshot/upload/upload.go b/snapshot/upload/upload.go index 63b8d03152e..c6975b53766 100644 --- a/snapshot/upload/upload.go +++ b/snapshot/upload/upload.go @@ -831,7 +831,7 @@ func (u *Uploader) processDirectoryEntries( return nil } -//nolint:funlen +//nolint:funlen,gocyclo func (u *Uploader) processSingle( ctx context.Context, entry fs.Entry, @@ -904,8 +904,8 @@ func (u *Uploader) processSingle( return nil case fs.Symlink: - childTree := policyTree.Child(entry.Name()) - de, err := u.uploadSymlinkInternal(ctx, entryRelativePath, entry, childTree.EffectivePolicy().MetadataCompressionPolicy.MetadataCompressor()) + compressor := policyTree.Child(entry.Name()).EffectivePolicy().MetadataCompressionPolicy.MetadataCompressor() + de, err := u.uploadSymlinkInternal(ctx, entryRelativePath, entry, compressor) return u.processEntryUploadResult(ctx, de, err, entryRelativePath, parentDirBuilder, policyTree.EffectivePolicy().ErrorHandlingPolicy.IgnoreFileErrors.OrDefault(false), @@ -928,8 +928,13 @@ func (u *Uploader) processSingle( prefix string ) + // Use the child policy for the specific entry path, not the parent directory policy. + // This ensures per-entry error handling rules are respected, consistent with how + // directory processing derives childTree via policyTree.Child(). + ehp := policyTree.Child(entry.Name()).EffectivePolicy().ErrorHandlingPolicy + if errors.Is(entry.ErrorInfo(), fs.ErrUnknown) { - isIgnoredError = policyTree.EffectivePolicy().ErrorHandlingPolicy.IgnoreUnknownTypes.OrDefault(true) + isIgnoredError = ehp.IgnoreUnknownTypes.OrDefault(true) // If unknown types are configured to be ignored, skip them completely without any error reporting if isIgnoredError { @@ -938,8 +943,13 @@ func (u *Uploader) processSingle( prefix = "unknown entry" } else { - isIgnoredError = policyTree.EffectivePolicy().ErrorHandlingPolicy.IgnoreFileErrors.OrDefault(false) prefix = "error" + + if entry.IsDir() { + isIgnoredError = ehp.IgnoreDirectoryErrors.OrDefault(false) + } else { + isIgnoredError = ehp.IgnoreFileErrors.OrDefault(false) + } } return u.processEntryUploadResult(ctx, nil, entry.ErrorInfo(), entryRelativePath, parentDirBuilder, diff --git a/snapshot/upload/upload_test.go b/snapshot/upload/upload_test.go index 5e1491b3b49..3ad253a313a 100644 --- a/snapshot/upload/upload_test.go +++ b/snapshot/upload/upload_test.go @@ -427,9 +427,9 @@ func TestUpload_ErrorEntries(t *testing.T) { t.Cleanup(th.cleanup) - th.sourceDir.Subdir("d1").AddErrorEntry("some-unknown-entry", os.ModeIrregular, fs.ErrUnknown) - th.sourceDir.Subdir("d1").AddErrorEntry("some-failed-entry", 0, errors.New("some-other-error")) - th.sourceDir.Subdir("d2").AddErrorEntry("another-failed-entry", os.ModeIrregular, errors.New("another-error")) + th.sourceDir.Subdir("d1").AddErrorEntryIrregular("some-unknown-entry", 0, fs.ErrUnknown) + th.sourceDir.Subdir("d1").AddErrorEntryFile("some-failed-entry", 0, errors.New("some-other-error")) + th.sourceDir.Subdir("d2").AddErrorEntryIrregular("another-failed-entry", 0, errors.New("another-error")) trueValue := policy.OptionalBool(true) falseValue := policy.OptionalBool(false) @@ -520,6 +520,117 @@ func TestUpload_ErrorEntries(t *testing.T) { } } +func TestUpload_ErrorEntryChildPolicy(t *testing.T) { + t.Parallel() + + ctx := testlogging.Context(t) + th := newUploadTestHarness(ctx, t) + + t.Cleanup(th.cleanup) + + // Add a dir-typed error entry, a file-typed error entry, and an unknown-typed error entry under d1. + th.sourceDir.Subdir("d1").AddErrorEntryDir("dir-err", 0, errors.New("dir-error")) + th.sourceDir.Subdir("d1").AddErrorEntryFile("file-err", 0, errors.New("file-error")) + th.sourceDir.Subdir("d1").AddErrorEntryIrregular("unknown-err", 0, fs.ErrUnknown) + + trueValue := policy.OptionalBool(true) + falseValue := policy.OptionalBool(false) + + cases := []struct { + desc string + defined map[string]*policy.Policy + defaultPolicy *policy.Policy + wantFatalErrors int + wantIgnoredErrors int + wantErrors entryPathToError + }{ + { + desc: "child policy ignores dir errors only", + defined: map[string]*policy.Policy{ + "./d1/dir-err": { + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreDirectoryErrors: &trueValue, + IgnoreFileErrors: &falseValue, + }, + }, + }, + defaultPolicy: &policy.Policy{ + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreDirectoryErrors: &falseValue, + IgnoreFileErrors: &falseValue, + }, + }, + wantFatalErrors: 1, // file-err is fatal (uses default policy) + wantIgnoredErrors: 1, // dir-err is ignored (uses child policy) + // unknown-err is silently skipped (IgnoreUnknownTypes defaults to true) + wantErrors: entryPathToError{ + "d1/dir-err": errors.New("dir-error"), + "d1/file-err": errors.New("file-error"), + }, + }, + { + desc: "child policy ignores file errors only", + defined: map[string]*policy.Policy{ + "./d1/file-err": { + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreDirectoryErrors: &falseValue, + IgnoreFileErrors: &trueValue, + }, + }, + }, + defaultPolicy: &policy.Policy{ + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreDirectoryErrors: &falseValue, + IgnoreFileErrors: &falseValue, + }, + }, + wantFatalErrors: 1, // dir-err is fatal (uses default policy) + wantIgnoredErrors: 1, // file-err is ignored (uses child policy) + // unknown-err is silently skipped (IgnoreUnknownTypes defaults to true) + wantErrors: entryPathToError{ + "d1/dir-err": errors.New("dir-error"), + "d1/file-err": errors.New("file-error"), + }, + }, + { + desc: "child policy disables unknown type ignore", + defined: map[string]*policy.Policy{ + "./d1/unknown-err": { + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreUnknownTypes: &falseValue, + }, + }, + }, + defaultPolicy: &policy.Policy{ + ErrorHandlingPolicy: policy.ErrorHandlingPolicy{ + IgnoreDirectoryErrors: &trueValue, + IgnoreFileErrors: &trueValue, + }, + }, + wantFatalErrors: 1, // unknown-err is fatal (child policy overrides default) + wantIgnoredErrors: 2, // dir-err and file-err are ignored (default policy) + wantErrors: entryPathToError{ + "d1/dir-err": errors.New("dir-error"), + "d1/file-err": errors.New("file-error"), + "d1/unknown-err": fs.ErrUnknown, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + u := NewUploader(th.repo) + + policyTree := policy.BuildTree(tc.defined, tc.defaultPolicy) + + man, err := u.Upload(ctx, th.sourceDir, policyTree, snapshot.SourceInfo{}) + require.NoError(t, err) + + verifyErrors(t, man, tc.wantFatalErrors, tc.wantIgnoredErrors, tc.wantErrors) + }) + } +} + func verifyErrors(t *testing.T, man *snapshot.Manifest, wantFatalErrors, wantIgnoredErrors int, wantErrors entryPathToError) { t.Helper() @@ -1347,7 +1458,7 @@ func TestUploadLogging(t *testing.T) { sourceDir.AddFile("f2", []byte{1, 2, 3, 4}, defaultPermissions) sourceDir.AddFile("f3", []byte{1, 2, 3, 4, 5}, defaultPermissions) sourceDir.AddSymlink("f4", "f2", defaultPermissions) - sourceDir.AddErrorEntry("f5", defaultPermissions, errors.New("some error")) + sourceDir.AddErrorEntryFile("f5", defaultPermissions, errors.New("some error")) sourceDir.AddDir("d1", defaultPermissions) sourceDir.AddDir("d1/d3", defaultPermissions) diff --git a/tests/end_to_end_test/server_start_test.go b/tests/end_to_end_test/server_start_test.go index 1cdb7fe02d3..4d8dc49f9cf 100644 --- a/tests/end_to_end_test/server_start_test.go +++ b/tests/end_to_end_test/server_start_test.go @@ -15,6 +15,7 @@ import ( "github.com/kopia/kopia/internal/apiclient" "github.com/kopia/kopia/internal/clock" + "github.com/kopia/kopia/internal/insecureserverbind" "github.com/kopia/kopia/internal/retry" "github.com/kopia/kopia/internal/serverapi" "github.com/kopia/kopia/internal/testlogging" @@ -545,6 +546,51 @@ func TestServerStartInsecure(t *testing.T) { e.RunAndExpectFailure(t, "server", "start", "--ui", "--address=localhost:0") } +func TestServerStartInsecureUnauthenticatedNonLoopbackRejected(t *testing.T) { + t.Parallel() + + runner := testenv.NewInProcRunner(t) + e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner) + + defer e.RunAndExpectSuccess(t, "repo", "disconnect") + + e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--override-hostname=fake-hostname", "--override-username=fake-username") + + e.RunAndExpectFailure(t, "server", "start", "--ui", + "--address=http://0.0.0.0:0", + "--without-password", + "--insecure", + ) +} + +func TestServerStartInsecureUnauthenticatedEscapeHatchNonLoopback(t *testing.T) { + t.Parallel() + + runner := testenv.NewInProcRunner(t) + e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner) + + defer e.RunAndExpectSuccess(t, "repo", "disconnect") + + e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--override-hostname=fake-hostname", "--override-username=fake-username") + + var sp testutil.ServerParameters + + wait, kill := e.RunAndProcessStderr(t, sp.ProcessOutput, + "server", "start", + "--address=http://0.0.0.0:0", + "--without-password", + "--insecure", + "--"+insecureserverbind.AllowDangerousUnauthenticatedNetworkFlag, + ) + + require.Eventually(t, func() bool { return sp.BaseURL != "" }, 15*time.Second, 50*time.Millisecond) + + kill() + wait() + + require.NotEmpty(t, sp.BaseURL) +} + func verifyServerConnected(t *testing.T, cli *apiclient.KopiaAPIClient, want bool) *serverapi.StatusResponse { t.Helper() diff --git a/tests/end_to_end_test/shallowrestore_test.go b/tests/end_to_end_test/shallowrestore_test.go index 516b76bbbe5..4fef2870b0a 100644 --- a/tests/end_to_end_test/shallowrestore_test.go +++ b/tests/end_to_end_test/shallowrestore_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/kopia/kopia/fs/localfs" - "github.com/kopia/kopia/internal/atomicfile" + "github.com/kopia/kopia/internal/ospath" "github.com/kopia/kopia/repo/object" "github.com/kopia/kopia/snapshot" "github.com/kopia/kopia/snapshot/restore" @@ -146,7 +146,7 @@ func TestShallowFullCycle(t *testing.T) { fpathinlong := filepath.Join(dirpathlong, "nestedfile") require.NoError(t, os.Mkdir(dirpathlong, 0o755)) - testdirtree.MustCreateRandomFile(t, atomicfile.MaybePrefixLongFilenameOnWindows(fpathinlong), testdirtree.DirectoryTreeOptions{}, (*testdirtree.DirectoryTreeCounters)(nil)) + testdirtree.MustCreateRandomFile(t, ospath.SafeLongFilename(fpathinlong), testdirtree.DirectoryTreeOptions{}, (*testdirtree.DirectoryTreeCounters)(nil)) e.RunAndExpectSuccess(t, "snapshot", "create", source) sources := clitestutil.ListSnapshotsAndExpectSuccess(t, e) @@ -224,9 +224,8 @@ func addOneFile(m *mutatorArgs) { func doNothing(_ *mutatorArgs) { } -// mplfow makes atomicfile.MaybePrefixLongFilenameOnWindows easier to type. -func mplfow(fname string) string { - return atomicfile.MaybePrefixLongFilenameOnWindows(fname) +func longName(fname string) string { + return ospath.SafeLongFilename(fname) } // moveDirectory moves a directory from one location to another (in the @@ -252,11 +251,11 @@ func moveDirectory(m *mutatorArgs) { require.NoError(m.t, os.Mkdir(neworiginaldir, 0o755)) // 3. move shallow dir into new dir, original dir into new dir - require.NoError(m.t, os.Rename(mplfow(dirinshallow), mplfow(filepath.Join(newshallowdir, relpath)))) - require.NoError(m.t, os.Rename(mplfow(filepath.Join(m.original, relpathinreal)), mplfow(filepath.Join(neworiginaldir, relpathinreal)))) + require.NoError(m.t, os.Rename(longName(dirinshallow), longName(filepath.Join(newshallowdir, relpath)))) + require.NoError(m.t, os.Rename(longName(filepath.Join(m.original, relpathinreal)), longName(filepath.Join(neworiginaldir, relpathinreal)))) // 4. fix new directory timestamp to be the same - fi, err := os.Stat(mplfow(newshallowdir)) + fi, err := os.Stat(longName(newshallowdir)) require.NoError(m.t, err) require.NoError(m.t, os.Chtimes(neworiginaldir, fi.ModTime(), fi.ModTime())) require.NoError(m.t, os.Chtimes(newshallowdir, fi.ModTime(), fi.ModTime())) @@ -283,11 +282,11 @@ func moveFile(m *mutatorArgs) { require.NoError(m.t, os.Mkdir(neworiginaldir, 0o755)) // 3. move shallow file into new dir, original dir into new dir - require.NoError(m.t, os.Rename(mplfow(fileinshallow), mplfow(filepath.Join(newshallowdir, relpath)))) - require.NoError(m.t, os.Rename(mplfow(filepath.Join(m.original, localfs.TrimShallowSuffix(relpath))), mplfow(filepath.Join(neworiginaldir, localfs.TrimShallowSuffix(relpath))))) + require.NoError(m.t, os.Rename(longName(fileinshallow), longName(filepath.Join(newshallowdir, relpath)))) + require.NoError(m.t, os.Rename(longName(filepath.Join(m.original, localfs.TrimShallowSuffix(relpath))), longName(filepath.Join(neworiginaldir, localfs.TrimShallowSuffix(relpath))))) // 4. fix new directory timestamp to be the same - fi, err := os.Stat(mplfow(newshallowdir)) + fi, err := os.Stat(longName(newshallowdir)) require.NoError(m.t, err) require.NoError(m.t, os.Chtimes(neworiginaldir, fi.ModTime(), fi.ModTime())) require.NoError(m.t, os.Chtimes(newshallowdir, fi.ModTime(), fi.ModTime())) diff --git a/tests/end_to_end_test/snapshot_fail_test.go b/tests/end_to_end_test/snapshot_fail_test.go index 33b734eba72..2c2fbd47f8e 100644 --- a/tests/end_to_end_test/snapshot_fail_test.go +++ b/tests/end_to_end_test/snapshot_fail_test.go @@ -82,7 +82,6 @@ func testSnapshotFailText(t *testing.T, isFailFast bool, snapshotCreateFlags []s testSnapshotFail(t, isFailFast, snapshotCreateFlags, snapshotCreateEnv, parseSnapshotResultFromLog) } -//nolint:thelper,cyclop func testSnapshotFail( t *testing.T, isFailFast bool, @@ -90,6 +89,8 @@ func testSnapshotFail( snapshotCreateEnv map[string]string, parseSnapshotResultFn func(t *testing.T, stdOut, _ []string) parsedSnapshotResult, ) { + t.Helper() + if runtime.GOOS == windowsOSName { t.Skip("this test does not work on Windows") } @@ -98,195 +99,228 @@ func testSnapshotFail( t.Skip("this test does not work as root, because we're unable to remove permissions.") } - const dir0Path = "dir0" - for _, ignoreFileErr := range []string{"true", "false"} { - for _, ignoreDirErr := range []string{"true", "false"} { - ignoringDirs := ignoreDirErr == "true" - ignoringFiles := ignoreFileErr == "true" + // Use "inherit" instead of "false" sometimes. Inherit defaults to false + if ignoreFileErr == "false" && rand.Intn(2) == 0 { + ignoreFileErr = "inherit" + } - // Use "inherit" instead of "false" sometimes. Inherit defaults to false - if !ignoringFiles && rand.Intn(2) == 0 { - ignoreFileErr = "inherit" - } + t.Run(fmt.Sprintf("failFast=%v:ignoreFileErr=%s", isFailFast, ignoreFileErr), func(t *testing.T) { + t.Parallel() - if !ignoringDirs && rand.Intn(2) == 0 { - ignoreDirErr = "inherit" - } + for _, ignoreDirErr := range []string{"true", "false"} { + if ignoreDirErr == "false" && rand.Intn(2) == 0 { + ignoreDirErr = "inherit" + } - var ( - expectedSuccess = expectedSnapshotResult{success: true} - expectEarlyFailure = expectedSnapshotResult{success: false} - expectedWhenIgnoringFiles = expectedSnapshotResult{success: ignoringFiles, wantErrors: cond(ignoringFiles, 0, 1), wantIgnoredErrors: cond(ignoringFiles, 1, 0), wantPartial: !ignoringFiles && isFailFast} - expectedWhenIgnoringDirs = expectedSnapshotResult{success: ignoringDirs, wantErrors: cond(ignoringDirs, 0, 1), wantIgnoredErrors: cond(ignoringDirs, 1, 0), wantPartial: !ignoringDirs && isFailFast} - ) - - // Test the root dir permissions - for tcIdx, tc := range []struct { - desc string - modifyEntry string - snapSource string - expectSuccess map[os.FileMode]expectedSnapshotResult - }{ - { - desc: "Modify permissions of the parent dir of the snapshot source (source is a FILE)", - modifyEntry: dir0Path, - snapSource: filepath.Join(dir0Path, "file1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, // --- permission: cannot read directory - 0o100: expectedSuccess, // --X permission: can enter directory and take snapshot of the file (with full permissions) - 0o400: expectEarlyFailure, // R-- permission: can read the file name, but will be unable to snapshot it without entering directory - }, - }, - { - desc: "Modify permissions of the parent dir of the snapshot source (source is a DIRECTORY)", - modifyEntry: dir0Path, - snapSource: filepath.Join(dir0Path, "dir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, - 0o100: expectedSuccess, - 0o400: expectEarlyFailure, - }, - }, - { - desc: "Modify permissions of the parent dir of the snapshot source (source is an EMPTY directory)", - modifyEntry: dir0Path, - snapSource: filepath.Join(dir0Path, "emptyDir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, - 0o100: expectedSuccess, - 0o400: expectEarlyFailure, - }, - }, - { - desc: "Modify permissions of the snapshot source itself (source is a FILE)", - modifyEntry: filepath.Join(dir0Path, "file1"), - snapSource: filepath.Join(dir0Path, "file1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, - 0o100: expectEarlyFailure, - 0o400: expectedSuccess, - }, - }, - { - desc: "Modify permissions of the snapshot source itself (source is a DIRECTORY)", - modifyEntry: filepath.Join(dir0Path, "dir1"), - snapSource: filepath.Join(dir0Path, "dir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, - 0o100: expectEarlyFailure, - 0o400: expectEarlyFailure, - }, - }, - { - desc: "Modify permissions of the snapshot source itself (source is an EMPTY directory)", - modifyEntry: filepath.Join(dir0Path, "emptyDir1"), - snapSource: filepath.Join(dir0Path, "emptyDir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectEarlyFailure, - 0o100: expectEarlyFailure, - 0o400: expectedSuccess, - }, - }, - { - desc: "Modify permissions of a FILE in the snapshot directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "file2"), - snapSource: filepath.Join(dir0Path, "dir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringFiles, - 0o100: expectedWhenIgnoringFiles, - 0o400: expectedSuccess, - }, - }, - { - desc: "Modify permissions of a DIRECTORY in the snapshot directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "dir2"), - snapSource: filepath.Join(dir0Path, "dir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringDirs, - 0o100: expectedWhenIgnoringDirs, - 0o400: expectedWhenIgnoringDirs, - }, - }, - { - desc: "Modify permissions of an EMPTY directory in the snapshot directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "emptyDir2"), - snapSource: filepath.Join(dir0Path, "dir1"), - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringDirs, - 0o100: expectedWhenIgnoringDirs, - 0o400: expectedSuccess, - }, - }, - { - desc: "Modify permissions of a FILE in a subdirectory of the snapshot root directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "file2"), - snapSource: dir0Path, - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringFiles, - 0o100: expectedWhenIgnoringFiles, - 0o400: expectedSuccess, - }, - }, - { - desc: "Modify permissions of a DIRECTORY in a subdirectory of the snapshot root directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "dir2"), - snapSource: dir0Path, - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringDirs, - 0o100: expectedWhenIgnoringDirs, - 0o400: expectedWhenIgnoringDirs, - }, - }, - { - desc: "Modify permissions of an EMPTY directory in a subdirectory of the snapshot root directory", - modifyEntry: filepath.Join(dir0Path, "dir1", "emptyDir2"), - snapSource: dir0Path, - expectSuccess: map[os.FileMode]expectedSnapshotResult{ - 0o000: expectedWhenIgnoringDirs, - 0o100: expectedWhenIgnoringDirs, - 0o400: expectedSuccess, - }, - }, - } { - // Reference test conditions outside of range variables to satisfy linter - tcIgnoreDirErr := ignoreDirErr - tcIgnoreFileErr := ignoreFileErr - tname := fmt.Sprintf("%s_ignoreFileErr_%s_ignoreDirErr_%s_failFast_%v", tc.desc, ignoreDirErr, ignoreFileErr, isFailFast) - - t.Run(tname, func(t *testing.T) { + t.Run("ignoreDirErr="+ignoreDirErr, func(t *testing.T) { t.Parallel() - runner := testenv.NewInProcRunner(t) - e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner) + testSnapshotFailCases(t, isFailFast, ignoreDirErr, ignoreFileErr, + snapshotCreateFlags, snapshotCreateEnv, parseSnapshotResultFn) + }) + } + }) + } +} - defer e.RunAndExpectSuccess(t, "repo", "disconnect") +//nolint:cyclop +func testSnapshotFailCases( + t *testing.T, + isFailFast bool, + ignoreDirErr string, + ignoreFileErr string, + snapshotCreateFlags []string, + snapshotCreateEnv map[string]string, + parseSnapshotResultFn func(t *testing.T, stdOut, stderr []string) parsedSnapshotResult, +) { + t.Helper() - e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir) + const dir0Path = "dir0" + + var ( + ignoringDirs = ignoreDirErr == "true" + ignoringFiles = ignoreFileErr == "true" + expectedSuccess = expectedSnapshotResult{success: true} + expectEarlyFailure = expectedSnapshotResult{success: false} + + expectedWhenIgnoringFiles = expectedSnapshotResult{ + success: ignoringFiles, + wantErrors: cond(ignoringFiles, 0, 1), + wantIgnoredErrors: cond(ignoringFiles, 1, 0), + wantPartial: !ignoringFiles && isFailFast, + } - scratchDir := testutil.TempDirectory(t) + expectedWhenIgnoringDirs = expectedSnapshotResult{ + success: ignoringDirs, + wantErrors: cond(ignoringDirs, 0, 1), + wantIgnoredErrors: cond(ignoringDirs, 1, 0), + wantPartial: !ignoringDirs && isFailFast, + } + + expectedWhenUnreadableDirEntries = expectedSnapshotResult{ + success: ignoringFiles && ignoringDirs, + wantErrors: fatalErrorCount(ignoringDirs, ignoringFiles), + wantIgnoredErrors: ignoredErrorCount(ignoringDirs, ignoringFiles), + wantPartial: !(ignoringFiles && ignoringDirs) && isFailFast, //nolint:staticcheck + } + ) - snapSource := filepath.Join(scratchDir, tc.snapSource) - modifyEntry := filepath.Join(scratchDir, tc.modifyEntry) + // Test the root dir permissions + cases := []struct { + desc string + modifyEntry string + snapSource string + expectSuccess map[os.FileMode]expectedSnapshotResult + }{ + { + desc: "Modify permissions of the parent dir of the snapshot source (source is a FILE)", + modifyEntry: dir0Path, + snapSource: filepath.Join(dir0Path, "file1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, // --- permission: cannot read directory + 0o100: expectedSuccess, // --X permission: can enter directory and take snapshot of the file (with full permissions) + 0o400: expectEarlyFailure, // R-- permission: can read the file name, but will be unable to snapshot it without entering directory + }, + }, + { + desc: "Modify permissions of the parent dir of the snapshot source (source is a DIRECTORY)", + modifyEntry: dir0Path, + snapSource: filepath.Join(dir0Path, "dir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, + 0o100: expectedSuccess, + 0o400: expectEarlyFailure, + }, + }, + { + desc: "Modify permissions of the parent dir of the snapshot source (source is an EMPTY directory)", + modifyEntry: dir0Path, + snapSource: filepath.Join(dir0Path, "emptyDir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, + 0o100: expectedSuccess, + 0o400: expectEarlyFailure, + }, + }, + { + desc: "Modify permissions of the snapshot source itself (source is a FILE)", + modifyEntry: filepath.Join(dir0Path, "file1"), + snapSource: filepath.Join(dir0Path, "file1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, + 0o100: expectEarlyFailure, + 0o400: expectedSuccess, + }, + }, + { + desc: "Modify permissions of the snapshot source itself (source is a DIRECTORY)", + modifyEntry: filepath.Join(dir0Path, "dir1"), + snapSource: filepath.Join(dir0Path, "dir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, + 0o100: expectEarlyFailure, + 0o400: expectedWhenUnreadableDirEntries, + }, + }, + { + desc: "Modify permissions of the snapshot source itself (source is an EMPTY directory)", + modifyEntry: filepath.Join(dir0Path, "emptyDir1"), + snapSource: filepath.Join(dir0Path, "emptyDir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectEarlyFailure, + 0o100: expectEarlyFailure, + 0o400: expectedSuccess, + }, + }, + { + desc: "Modify permissions of a FILE in the snapshot directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "file2"), + snapSource: filepath.Join(dir0Path, "dir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringFiles, + 0o100: expectedWhenIgnoringFiles, + 0o400: expectedSuccess, + }, + }, + { + desc: "Modify permissions of a DIRECTORY in the snapshot directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "dir2"), + snapSource: filepath.Join(dir0Path, "dir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringDirs, + 0o100: expectedWhenIgnoringDirs, + 0o400: expectedWhenUnreadableDirEntries, + }, + }, + { + desc: "Modify permissions of an EMPTY directory in the snapshot directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "emptyDir2"), + snapSource: filepath.Join(dir0Path, "dir1"), + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringDirs, + 0o100: expectedWhenIgnoringDirs, + 0o400: expectedSuccess, + }, + }, + { + desc: "Modify permissions of a FILE in a subdirectory of the snapshot root directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "file2"), + snapSource: dir0Path, + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringFiles, + 0o100: expectedWhenIgnoringFiles, + 0o400: expectedSuccess, + }, + }, + { + desc: "Modify permissions of a DIRECTORY in a subdirectory of the snapshot root directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "dir2"), + snapSource: dir0Path, + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringDirs, + 0o100: expectedWhenIgnoringDirs, + 0o400: expectedWhenUnreadableDirEntries, + }, + }, + { + desc: "Modify permissions of an EMPTY directory in a subdirectory of the snapshot root directory", + modifyEntry: filepath.Join(dir0Path, "dir1", "emptyDir2"), + snapSource: dir0Path, + expectSuccess: map[os.FileMode]expectedSnapshotResult{ + 0o000: expectedWhenIgnoringDirs, + 0o100: expectedWhenIgnoringDirs, + 0o400: expectedSuccess, + }, + }, + } - // Each directory tier will have a file, an empty directory, and the next tier's directory - // (unless at max depth). Naming scheme is [file|dir|emptyDir][tier #]. - createSimplestFileTree(t, 3, 0, scratchDir) + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + testPermissions(t, tc.snapSource, tc.modifyEntry, ignoreDirErr, ignoreFileErr, tc.expectSuccess, snapshotCreateFlags, snapshotCreateEnv, parseSnapshotResultFn) + }) + } +} - restoreDirPrefix := filepath.Join(scratchDir, "target") +func ignoredErrorCount(ignoringDirErrs, ignoringFileErrs bool) int { + var errCount int - e.RunAndExpectSuccess(t, "policy", "set", snapSource, "--ignore-dir-errors", tcIgnoreDirErr, "--ignore-file-errors", tcIgnoreFileErr) - restoreDir := fmt.Sprintf("%s%d_%v_%v", restoreDirPrefix, tcIdx, tcIgnoreDirErr, tcIgnoreFileErr) - testPermissions(t, e, snapSource, modifyEntry, restoreDir, tc.expectSuccess, snapshotCreateFlags, snapshotCreateEnv, parseSnapshotResultFn) + if ignoringDirErrs { + errCount += 2 + } - e.RunAndExpectSuccess(t, "policy", "remove", snapSource) - }) - } - } + if ignoringFileErrs { + errCount += 1 } + + return errCount } -func createSimplestFileTree(t *testing.T, dirDepth, currDepth int, currPath string) { +func fatalErrorCount(ignoringDirErrs, ignoringFileErrs bool) int { + return 3 - ignoredErrorCount(ignoringDirErrs, ignoringFileErrs) +} + +func createSimplestFileTree(t *testing.T, maxDirDepth, currDepth int, currPath string) { t.Helper() dirname := fmt.Sprintf("dir%d", currDepth) @@ -294,6 +328,10 @@ func createSimplestFileTree(t *testing.T, dirDepth, currDepth int, currPath stri err := os.MkdirAll(dirPath, 0o700) require.NoError(t, err) + if currDepth >= maxDirDepth { + return + } + // Put an empty directory in the new directory emptyDirName := fmt.Sprintf("emptyDir%v", currDepth+1) emptyDirPath := filepath.Join(dirPath, emptyDirName) @@ -304,91 +342,98 @@ func createSimplestFileTree(t *testing.T, dirDepth, currDepth int, currPath stri fileName := fmt.Sprintf("file%d", currDepth+1) filePath := filepath.Join(dirPath, fileName) - testdirtree.MustCreateRandomFile(t, filePath, testdirtree.DirectoryTreeOptions{}, nil) + testdirtree.MustCreateRandomFile(t, filePath, testdirtree.DirectoryTreeOptions{MaxFileSize: 8}, nil) - if dirDepth > currDepth+1 { - createSimplestFileTree(t, dirDepth, currDepth+1, dirPath) - } + createSimplestFileTree(t, maxDirDepth, currDepth+1, dirPath) } -// testPermissions iterates over readable and executable permission states, testing -// files and directories (if present). It issues the kopia snapshot command -// against "source" and will test permissions against all entries in "parentDir". -// It returns the number of successful snapshot operations. -// -//nolint:thelper +// testPermissions verifies that a kopia snapshot command returns +// the expected result when the access permissions of modifyEntry +// are set according to the expect map. +// It iterates over the permissions (keys in the expect map), changes +// the permissions for modifyEntry, then issues a kopia snapshot command +// against the modified source and verifies that the command returns +// the corresponding expectedSnapshotResult (value in the expect map). func testPermissions( t *testing.T, - e *testenv.CLITest, - source, modifyEntry, restoreDir string, + tcSnapSource, tcModifyEntry, ignoreDirErr, ignoreFileErr string, expect map[os.FileMode]expectedSnapshotResult, snapshotCreateFlags []string, snapshotCreateEnv map[string]string, parseSnapshotResultFn func(_ *testing.T, _, _ []string) parsedSnapshotResult, -) int { - var numSuccessfulSnapshots int +) { + t.Helper() + + runner := testenv.NewInProcRunner(t) + e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner) + + e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir) + defer e.RunAndExpectSuccess(t, "repo", "disconnect") + + scratchDir := testutil.TempDirectory(t) + source := filepath.Join(scratchDir, tcSnapSource) + + // Each directory tier will have a file, an empty directory, and the next tier's directory + // (unless at max depth). Naming scheme is [file|dir|emptyDir][tier #]. + createSimplestFileTree(t, 3, 0, scratchDir) + + e.RunAndExpectSuccess(t, "policy", "set", source, "--ignore-dir-errors", ignoreDirErr, "--ignore-file-errors", ignoreFileErr) + defer e.RunAndExpectSuccess(t, "policy", "remove", source) + + modifyEntry := filepath.Join(scratchDir, tcModifyEntry) changeFile, err := os.Stat(modifyEntry) require.NoError(t, err) - // Iterate over all permission bit configurations - for chmod, expected := range expect { - // run in nested function go be able to do defer - func() { - mode := changeFile.Mode() + prevPerms := changeFile.Mode().Perm() - // restore permissions even if we fail to avoid leaving non-deletable files behind. - defer func() { - t.Logf("restoring file mode on %s to %v", modifyEntry, mode) - require.NoError(t, os.Chmod(modifyEntry, mode.Perm())) - }() + // save environment so it can be restored after each subtest modifies it + oldEnv := e.Environment + defer func() { e.Environment = oldEnv }() + + // Iterate over all permission bit configurations + for permissions, expected := range expect { + t.Run("mode:"+permissions.String(), func(t *testing.T) { + t.Cleanup(func() { + // restore permissions even if we fail to avoid leaving non-deletable files behind. + require.NoErrorf(t, os.Chmod(modifyEntry, prevPerms), "restoring file mode on %s to %v", modifyEntry, prevPerms) + }) - t.Logf("Chmod: path: %s, isDir: %v, prevMode: %v, newMode: %v", modifyEntry, changeFile.IsDir(), mode, chmod) + t.Logf("Chmod: path: %s, isDir: %v, prevMode: %v, newMode: %v", modifyEntry, changeFile.IsDir(), prevPerms, permissions) - err := os.Chmod(modifyEntry, chmod) + err := os.Chmod(modifyEntry, permissions) require.NoError(t, err) // set up environment for the child process. - oldEnv := e.Environment - - e.Environment = map[string]string{} - - maps.Copy(e.Environment, oldEnv) + e.Environment = maps.Clone(oldEnv) maps.Copy(e.Environment, snapshotCreateEnv) - defer func() { e.Environment = oldEnv }() - snapshotCreateWithArgs := append([]string{"snapshot", "create", source}, snapshotCreateFlags...) stdOut, stdErr, runErr := e.Run(t, !expected.success, snapshotCreateWithArgs...) - if got, want := (runErr == nil), expected.success; got != want { - t.Fatalf("unexpected success %v, want %v", got, want) - } + require.Equalf(t, expected.success, (runErr == nil), "expected success: %t", expected.success) parsed := parseSnapshotResultFn(t, stdOut, stdErr) if expected.success { - numSuccessfulSnapshots++ + target := filepath.Join(t.TempDir(), "target") - e.RunAndExpectSuccess(t, "snapshot", "restore", parsed.manifestID, restoreDir) + e.RunAndExpectSuccess(t, "snapshot", "restore", parsed.manifestID, target) } - if got, want := parsed.errorCount, expected.wantErrors; got != want { - t.Fatalf("unexpected number of errors: %v, want %v", got, want) - } - - if got, want := parsed.ignoredErrorCount, expected.wantIgnoredErrors; got != want { - t.Fatalf("unexpected number of ignored errors: %v, want %v", got, want) - } + require.Equal(t, expected.wantPartial, parsed.partial, "unexpected partial") - if got, want := parsed.partial, expected.wantPartial; got != want { - t.Fatalf("unexpected partial %v, want %v (%s)", got, want, stdErr) + if expected.wantPartial { + // for partial snapshots, only check that at least one fatal error was recorded + require.Positive(t, parsed.errorCount, "expected at least one fatal error") + } else { + // the total number of errors can only be validated for non-partial snapshots + require.Equal(t, expected.wantErrors, parsed.errorCount, "unexpected number of errors") + require.Equal(t, expected.wantIgnoredErrors, parsed.ignoredErrorCount, "unexpected number of ignored errors") } - }() + }) } - - return numSuccessfulSnapshots } var ( @@ -422,16 +467,12 @@ func parseSnapshotResultFromLog(t *testing.T, _, stdErr []string) parsedSnapshot if match := fatalErrorsPattern.FindStringSubmatch(l); match != nil { res.errorCount, err = strconv.Atoi(match[1]) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } if match := ignoredErrorsPattern.FindStringSubmatch(l); match != nil { res.ignoredErrorCount, err = strconv.Atoi(match[1]) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } } diff --git a/tests/htmlui_e2e_test/htmlui_e2e_test.go b/tests/htmlui_e2e_test/htmlui_e2e_test.go index 1211c4e6c99..241f6dade48 100644 --- a/tests/htmlui_e2e_test/htmlui_e2e_test.go +++ b/tests/htmlui_e2e_test/htmlui_e2e_test.go @@ -207,6 +207,12 @@ func TestEndToEndTest(t *testing.T) { chromedp.Click("a[data-testid='tab-repo']"), tc.captureScreenshot("repository"), + chromedp.ActionFunc(func(context.Context) error { + t.Skip("Disconnect times out, skipping for now to unblock CI") + + return nil + }), + tc.log("disconnecting"), chromedp.Click("button[data-testid='disconnect']"), tc.captureScreenshot("disconnected"), @@ -255,6 +261,12 @@ func TestConnectDisconnectReconnect(t *testing.T) { chromedp.Click("a[data-testid='tab-repo']"), tc.captureScreenshot("repository"), + chromedp.ActionFunc(func(context.Context) error { + t.Skip("Disconnect times out, skipping for now to unblock CI") + + return nil + }), + tc.log("disconnecting"), chromedp.Click("button[data-testid='disconnect']"), tc.captureScreenshot("disconnected"), diff --git a/tests/robustness/multiclient_test/framework/client.go b/tests/robustness/multiclient_test/framework/client.go index 266b3c9cc7d..793b78ff2d5 100644 --- a/tests/robustness/multiclient_test/framework/client.go +++ b/tests/robustness/multiclient_test/framework/client.go @@ -20,10 +20,6 @@ type Client struct { ID string } -func init() { - petname.NonDeterministicMode() -} - func newClient() *Client { return &Client{ ID: petname.Generate(nameLen, "-") + "-" + uuid.NewString(), diff --git a/tools/cli2md/cli2md.go b/tools/cli2md/cli2md.go index a6e63c34053..e3d226c49e5 100644 --- a/tools/cli2md/cli2md.go +++ b/tools/cli2md/cli2md.go @@ -63,7 +63,7 @@ func emitFlags(w io.Writer, flags []*kingpin.FlagModel) { shortFlag := "" if f.Short != 0 { - shortFlag = "`-" + string([]byte{byte(f.Short)}) + "`" + shortFlag = "`-" + string(f.Short) + "`" } defaultValue := "" diff --git a/tools/gettool/checksums.txt b/tools/gettool/checksums.txt index 9d5e4d438d5..20a80e6df10 100644 --- a/tools/gettool/checksums.txt +++ b/tools/gettool/checksums.txt @@ -19,12 +19,12 @@ https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_L https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Linux_armv6.tar.gz: f1903865b6ede1a4324c71d3efa4155b7067d1d357ccfd844c07c2bb3dcb4af2 https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Linux_x86_64.tar.gz: 13bf8ef4ec33d4f3ff2d2c7c02361946e29d69093cf7102e46dcb49e48a31435 https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Windows_x86_64.zip: ccd955af3069c3f8a560e40b7d6a92566febeb5abb243274e4484c136ec7b4df -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_darwin_amd64.tar.gz: e857b31adde83a534cb7ae2b2eec73fed5d96687a25692267dd061e220df102e -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_darwin_arm64.tar.gz: 4e47a76a29150ff90638d249843c2d10c4ed6abdafdde5f8bf9fd9f19e36a3fd -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_linux_amd64.tar.gz: 531c37ec646a9793a3c473831b9ee5314da8056c263772840d96afe9a9498e93 -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_linux_arm64.tar.gz: 51c7fe29216678edaaa96bb67e38d58437fd54a83468f58a32513995f575dcc3 -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_linux_armv6.tar.gz: 79a6a904d73a7b6b010f82205803e0c0a8a202a63f51e93e555e2f9be8aa3ba3 -https://github.com/gotestyourself/gotestsum/releases/download/v1.11.0/gotestsum_1.11.0_windows_amd64.tar.gz: 1518b3dd6a44b5684e9732121933f52b9c3ccab3a6e9efdeac41e7b03f97d019 +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz: 99529350f4c7b780b1efc543ca0d9721b09f0a4228f0efa9281261f58fefa05a +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz: 509cb27aef747f48faf9bce424f59dcf79572c905204b990ee935bbfcc7fa0e9 +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz: 11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_arm64.tar.gz: 7644a4c5cd1bb978d56245aeab25a586ac5ac62adebed20a399548867c13499d +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_armv6.tar.gz: 5ba38a53b4d612aa2214508052c2e0230b36aec60eed6dba434476b720dcf4d6 +https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz: fd5a6dc69e46a0970593e70d85a7e75f16714e9c61d6d72ccc324eb82df5bb8a https://github.com/kopia/kopia/releases/download/v0.17.0/kopia-0.17.0-linux-arm.tar.gz: 25804d7271a0dfe6d0821270c5640caa01da5e05a03a7c4783fd1edafb234d51 https://github.com/kopia/kopia/releases/download/v0.17.0/kopia-0.17.0-linux-arm64.tar.gz: 9679415cd2717a90cb6a793aa2d4accde4059084245b27fa4807d7e13fbe40a0 https://github.com/kopia/kopia/releases/download/v0.17.0/kopia-0.17.0-linux-x64.tar.gz: 6851bba9f49c2ca2cabc5bec85a813149a180472d1e338fad42a8285dad047ee diff --git a/tools/htmlui_changelog.sh b/tools/htmlui_changelog.sh index c71b5e11d65..964dacf61ac 100755 --- a/tools/htmlui_changelog.sh +++ b/tools/htmlui_changelog.sh @@ -3,7 +3,7 @@ set -xe if [ -z "$CI_TAG" ]; then echo "CI_TAG is not set. Looking for previous tag." - start_commit=$(git describe --tags --abbrev=0 HEAD^) + start_commit=$(git describe --tags --abbrev=0 --always HEAD^) end_commit=HEAD else echo "CI_TAG is set to $CI_TAG. Using it as the start commit." diff --git a/tools/tools.mk b/tools/tools.mk index d91f479a9c1..bae9e62406f 100644 --- a/tools/tools.mk +++ b/tools/tools.mk @@ -6,7 +6,7 @@ # # you will need to have git and golang too in the PATH. -.PHONY: all-tools install-linter install-gotestsum +.PHONY: all-tools install-checklocks install-gotestsum install-linter # windows,linux,darwin GOOS:=$(shell go env GOOS) @@ -108,7 +108,7 @@ GOLANGCI_LINT_VERSION=2.6.1 CHECKLOCKS_VERSION=release-20241104.0 NODE_VERSION=22.15.1 HUGO_VERSION=0.113.0 -GOTESTSUM_VERSION=1.11.0 +GOTESTSUM_VERSION=1.13.0 GORELEASER_VERSION=v0.176.0 RCLONE_VERSION=1.68.2 GITCHGLOG_VERSION=0.15.1 @@ -168,6 +168,8 @@ $(checklocks): go install gvisor.dev/gvisor/tools/checklocks/cmd/checklocks@$(CHECKLOCKS_VERSION) go clean -modcache +install-checklocks: $(checklocks) + # cli2md cli2mdbin=$(TOOLS_DIR)$(slash)cli2md-current$(exe_suffix) @@ -322,4 +324,4 @@ regenerate-checksums: --output-dir /tmp/all-tools \ --tool $(ALL_TOOL_VERSIONS) -all-tools: install-gotestsum $(npm) install-linter $(maybehugo) +all-tools: install-gotestsum install-linter $(maybehugo) $(npm)