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

[](https://github.com/kopia/kopia/actions?query=workflow%3ABuild)
-[](https://slack.kopia.io/)
[](https://godoc.org/github.com/kopia/kopia/repo)
[](https://codecov.io/gh/kopia/kopia)[](https://goreportcard.com/report/github.com/kopia/kopia)
[](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`.
[](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 @@