diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..491cf7a9f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +## Based on microsoft go devcontainer - https://github.com/microsoft/vscode-dev-containers/blob/v0.205.2/containers/go/.devcontainer/Dockerfile +# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster +ARG VARIANT=1-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends protobuf-compiler + +USER vscode + +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \ + && go install github.com/bufbuild/buf/cmd/buf@v1.27.2 \ + && go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest \ + && go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \ + && go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest \ + && go install github.com/GeertJohan/go.rice/rice@latest \ + && go install github.com/goreleaser/goreleaser@latest \ + && npm install -g @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..df2503684 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,49 @@ +{ + "name": "Go", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.17 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "1-1.23-bookworm", + // Options + "NODE_VERSION": "lts/*" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "go.goroot": "/usr/local/go", + "typescript.tsdk": "webui/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "gitlens.telemetry.enabled": false + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "golang.Go", + "ms-azuretools.vscode-docker", + "mhutchie.git-graph", + "eamodio.gitlens", + "donjayamanne.githistory", + "esbenp.prettier-vscode", + "iulian-radu-at.vscode-tasks-sidebar" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..a74af7ffb --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +buy_me_a_coffee: garethgeorge +github: garethgeorge diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..fcc8ade04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Report an issue with Backrest +title: "" +labels: bug +assignees: "" +--- + +Note: if you have a question or need support please post in the [discussions area](https://github.com/garethgeorge/backrest/discussions). + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform Info** + +- OS and Architecture [e.g. Windows 10 x64, Darwin arm64] +- Backrest Version [e.g. 0.0.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..7c4af7e12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement +assignees: "" +--- + +Note: if you have a question or want discussion please post in the [discussions area](https://github.com/garethgeorge/backrest/discussions). + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..37a1172cd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Build Snapshot Release + +on: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "*.md" + pull_request: + branches: ["main"] + paths-ignore: + - "docs/**" + - "*.md" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Setup NodeJS + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Build + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --snapshot --clean + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: backrest-snapshot-builds + path: | + dist/*.tar.gz + dist/*.zip + + - name: Generate Installers + run: | + mkdir -p dist-installers + ./scripts/generate-installers.sh ./dist-installers + + - name: Upload Installers + uses: actions/upload-artifact@v4 + with: + name: backrest-snapshot-installers + path: dist-installers/*.exe diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..bf714c156 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Docs + +on: + push: + branches: ["main"] + paths: + - "docs/**" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up NodeJS + uses: actions/setup-node@v2 + with: + node-version: "20" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --prefix ./docs + + - name: Build + run: | + cd docs + pnpm run generate + ls -la .output/public + + - name: Fix permissions + run: | + chmod -c -R +rX "docs/.output/public" | while read line; do + echo "::warning title=Invalid file permissions automatically fixed::$line" + done + + - name: Upload static files as artifact + id: deployment + uses: actions/upload-pages-artifact@v3 # or specific "vX.X.X" version tag for this action + with: + path: docs/.output/public + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + actions: read + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 000000000..5d93e8871 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,19 @@ +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +name: Release Please + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + release-type: simple diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..11c8b9121 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Tagged Release + +on: + push: + tags: + - "*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + tagged-release: + name: "Tagged Release" + runs-on: "ubuntu-latest" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Setup NodeJS + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: dist/* + + tagged-release-installers: + name: "Tagged Release Installers" + runs-on: "ubuntu-latest" + needs: tagged-release + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # download dist artifacts from previous job + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: dist + + - name: Generate Installers + run: | + mkdir -p dist-installers + ./scripts/generate-installers.sh ./dist-installers + + - name: Upload Assets + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + ./dist-installers/Backrest-setup-x86_64.exe + ./dist-installers/Backrest-setup-arm64.exe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..36214090a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,62 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Test + +on: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "*.md" + pull_request: + branches: ["main"] + paths-ignore: + - "docs/**" + - "*.md" + workflow_dispatch: + +jobs: + test-nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + + - name: Create Fake WebUI Sources + run: | + mkdir -p webui/dist + touch webui/dist/index.html.gz + + - name: Build + run: go build ./... + + - name: Test + run: PATH=$(pwd):$PATH gotestsum ./... -- --race + + test-win: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Create Fake WebUI Sources + run: | + New-Item -Path .\webui\dist-windows\index.html -ItemType File -Force + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... diff --git a/.github/workflows/update-restic.yml b/.github/workflows/update-restic.yml new file mode 100644 index 000000000..b5b7f08c5 --- /dev/null +++ b/.github/workflows/update-restic.yml @@ -0,0 +1,34 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Update Restic + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-restic-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Check for updates + run: | + ./scripts/update-restic-version.sh + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore: update restic version" + title: "chore: update restic version" + body: Beep boop. Bot generated PR to update backrest to the latest restic version. + assignees: garethgeorge + branch: "update-restic-version" + base: "main" diff --git a/.gitignore b/.gitignore index 05077407b..1c079cb11 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ -cmd/resticui/resticui +test* +backrest-* +dist +__debug_bin +cmd/backrest/backrest +*.exe +.DS_Store +.idea/ \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000..1c75af1a3 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,199 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 1 + +env: + - BACKREST_BUILD_VERSION={{.Version}} + +before: + hooks: + - go mod tidy + - pnpm --prefix webui install + - sh -c "GOOS=linux BACKREST_BUILD_VERSION={{.Version}} go generate ./..." + - sh -c "GOOS=windows BACKREST_BUILD_VERSION={{.Version}} go generate ./..." + +builds: + - id: other + main: ./cmd/backrest + env: + - CGO_ENABLED=0 + goos: + - darwin + - windows + - freebsd + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + - id: linux + main: ./cmd/backrest + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + - arm + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + - id: backrestmon + main: ./cmd/backrestmon + binary: backrest-windows-tray + env: + - CGO_ENABLED=1 + - GO111MODULE=on + ldflags: -H=windowsgui + goos: + - windows + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip + files: + - install.sh + - uninstall.sh + - LICENSE + - README.md + - CHANGELOG.md + allow_different_binary_count: true + +dockers: + - image_templates: + - garethgeorge/backrest:{{ .Tag }}-alpine-amd64 + dockerfile: Dockerfile.alpine + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + + - image_templates: + - garethgeorge/backrest:{{ .Tag }}-alpine-arm64 + dockerfile: Dockerfile.alpine + goarch: arm64 + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm64/v8" + + - image_templates: + - garethgeorge/backrest:{{ .Tag }}-scratch-arm64 + dockerfile: Dockerfile.scratch + goarch: arm64 + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm64/v8" + + - image_templates: + - garethgeorge/backrest:{{ .Tag }}-scratch-amd64 + dockerfile: Dockerfile.scratch + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + + - image_templates: + - garethgeorge/backrest:{{ .Tag }}-scratch-armv6 + dockerfile: Dockerfile.scratch + use: buildx + goarch: arm + goarm: 6 + build_flag_templates: + - "--pull" + - "--platform=linux/arm/v6" + +docker_manifests: + - name_template: "garethgeorge/backrest:latest" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-alpine-amd64" + - "garethgeorge/backrest:{{ .Tag }}-alpine-arm64" + - name_template: "garethgeorge/backrest:v{{ .Major }}" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-alpine-amd64" + - "garethgeorge/backrest:{{ .Tag }}-alpine-arm64" + - name_template: "garethgeorge/backrest:v{{ .Major }}.{{ .Minor }}" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-alpine-amd64" + - "garethgeorge/backrest:{{ .Tag }}-alpine-arm64" + - name_template: "garethgeorge/backrest:{{ .Tag }}" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-alpine-amd64" + - "garethgeorge/backrest:{{ .Tag }}-alpine-arm64" + - name_template: "garethgeorge/backrest:latest-alpine" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-alpine-amd64" + - "garethgeorge/backrest:{{ .Tag }}-alpine-arm64" + - name_template: "garethgeorge/backrest:scratch" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" + - name_template: "garethgeorge/backrest:v{{ .Major }}-scratch" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" + - name_template: "garethgeorge/backrest:v{{ .Major }}.{{ .Minor }}-scratch" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" + - name_template: "garethgeorge/backrest:{{ .Tag }}-scratch" + image_templates: + - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" + - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" + +brews: + - name: backrest + homepage: https://github.com/garethgeorge/backrest + description: "Backrest is a web UI and orchestrator for restic backup." + + url_template: "https://github.com/garethgeorge/backrest/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + + repository: + owner: garethgeorge + name: homebrew-backrest-tap + branch: main + token: "{{ .Env.HOMEBREW_GITHUB_TOKEN }}" + + service: | + run opt_bin/"backrest" + error_log_path var/"log/backrest.log" + log_path var/"log/backrest.log" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: garethgeorge + name: backrest diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..6893fe39c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Go backend", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/backrest.go", + "output": "__debug_bin", + "preLaunchTask": "Build Webui" + }, + { + "type": "chrome", + "request": "launch", + "preLaunchTask": "Parcel", + "postDebugTask": "Terminate Parcel", + "name": "Debug TS frontend", + "url": "http://localhost:1234", + "webRoot": "${workspaceFolder}/webui/src/", + "sourceMapPathOverrides": { + "../*": "${webRoot}/*" + } + } + ], + "compounds": [ + { + "name": "Debug Backrest (backend+frontend)", + "configurations": ["Debug Go backend", "Debug TS frontend"], + "stopAll": true + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..7301b866e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,51 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "npm i", + "type": "shell", + "command": "cd webui && npm i" + }, + { + "label": "Parcel", + "type": "npm", + "script": "start", + "dependsOn": "npm i", + "isBackground": true, + "problemMatcher": { + "background": { + "activeOnStart": true, + "beginsPattern": "parcel serve", + "endsPattern": "Built in" + }, + "pattern": { + "regexp": ".*" + } + }, + "path": "webui" + }, + { + "label": "Build Webui", + "type": "npm", + "script": "build", + "path": "webui", + "group": "build", + "problemMatcher": [], + "dependsOn": "npm i" + }, + { + "label": "Terminate Parcel", + "command": "echo ${input:terminate}", + "type": "shell", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "terminate", + "type": "command", + "command": "workbench.action.tasks.terminate", + "args": "Parcel" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ecdb5b15e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,751 @@ +# Changelog + +## [1.8.0](https://github.com/garethgeorge/backrest/compare/v1.7.3...v1.8.0) (2025-04-02) + + +### Bug Fixes + +* deduplicate indexed snapshots ([#716](https://github.com/garethgeorge/backrest/issues/716)) ([b3b1eef](https://github.com/garethgeorge/backrest/commit/b3b1eefe9b07dbc782ad2a519f960834be1329b3)) +* glob escape some linux filename characters ([#721](https://github.com/garethgeorge/backrest/issues/721)) ([190b3bf](https://github.com/garethgeorge/backrest/commit/190b3bfd0e7debf274022b64e294204a94074d1f)) +* restic outputs add newline separators between log messages ([addf49c](https://github.com/garethgeorge/backrest/commit/addf49c1f37818b5e4be05db2982a0555703fa78)) +* update healthchecks hook to construct urls such that query parameters are preserved ([2a24b0a](https://github.com/garethgeorge/backrest/commit/2a24b0ad5fa3583686086e58744184fe07e3e657)) + + +### Miscellaneous Chores + +* release 1.8.0 for restic version upgrade to 0.18.0 ([ad2c357](https://github.com/garethgeorge/backrest/commit/ad2c357bc3c4d54e7290b8e6a24483a8afeba1f5)) + +## [1.7.3](https://github.com/garethgeorge/backrest/compare/v1.7.2...v1.7.3) (2025-03-15) + + +### Bug Fixes + +* add missing hooks for CONDITION_FORGET_{START, SUCCESS, ERROR} ([489c6f5](https://github.com/garethgeorge/backrest/commit/489c6f5b34d39d718f4ccf62ac155826685fa8d3)) +* add priority fields to gotify notifications ([#678](https://github.com/garethgeorge/backrest/issues/678)) ([ec95c4a](https://github.com/garethgeorge/backrest/commit/ec95c4a8a311f63f3c033f39c8633f50d2d47be9)) +* hook errors should be shown as warnings in tree view ([9f112bc](https://github.com/garethgeorge/backrest/commit/9f112bc78d7fc9609b5832b9f665dd55c9c28714)) +* improve exported prometheus metrics for task execution and status ([#684](https://github.com/garethgeorge/backrest/issues/684)) ([8bafe7e](https://github.com/garethgeorge/backrest/commit/8bafe7ea35e990377f96662fc81ccdcc34b4dda6)) +* index snapshots incorrectly creates duplicate entries for snapshots from other instances ([#693](https://github.com/garethgeorge/backrest/issues/693)) ([5ab7553](https://github.com/garethgeorge/backrest/commit/5ab755393a640090b659537de900988302d3e9ea)) +* occasional truncated operation history display in repo view ([3b41d9f](https://github.com/garethgeorge/backrest/commit/3b41d9fd5bd611dd0c59bcef13a3da0e2d6f02ce)) +* support AWS_SHARED_CREDENTIALS_FILE for s3 authentication ([154aef4](https://github.com/garethgeorge/backrest/commit/154aef4c9a26248ec7f09c731465647b5359a995)) + +## [1.7.2](https://github.com/garethgeorge/backrest/compare/v1.7.1...v1.7.2) (2025-02-16) + + +### Bug Fixes + +* convert prometheus metrics to use `gauge` type ([#640](https://github.com/garethgeorge/backrest/issues/640)) ([8c4ddee](https://github.com/garethgeorge/backrest/commit/8c4ddeea7132fb94484dc32872e00ddd3b35e44d)) +* hooks fail to populate a non-nil Plan variable for system tasks ([f119e1e](https://github.com/garethgeorge/backrest/commit/f119e1e979a464e508edcb13404691ad45ac3d64)) +* incorrectly formatted total size on stats panel ([#667](https://github.com/garethgeorge/backrest/issues/667)) ([d2ac114](https://github.com/garethgeorge/backrest/commit/d2ac1146ac0d2d75ca7dc51c9db076adc7170000)) +* misaligned favicon ([#660](https://github.com/garethgeorge/backrest/issues/660)) ([403458f](https://github.com/garethgeorge/backrest/commit/403458f70705258906258fa77d4668d70ac176e3)) +* more robust delete repo and misc repo guid related bug fixes ([146032a](https://github.com/garethgeorge/backrest/commit/146032a9d7a66c422318461b8113d6369c6cd640)) +* restore path on Windows ([#631](https://github.com/garethgeorge/backrest/issues/631)) ([1a9ecc5](https://github.com/garethgeorge/backrest/commit/1a9ecc58390957523f21c6a55c809b0bf22cf978)) +* snapshot still showing after forget until page is refreshed ([0600733](https://github.com/garethgeorge/backrest/commit/060073325dbbf6cc94ce6471134efb91fe191cca)) + +## [1.7.1](https://github.com/garethgeorge/backrest/compare/v1.7.0...v1.7.1) (2025-01-24) + + +### Bug Fixes + +* add favicon to webui ([#649](https://github.com/garethgeorge/backrest/issues/649)) ([dd1e18c](https://github.com/garethgeorge/backrest/commit/dd1e18c9cbe6ed6ba5788ea646fc99d50e41ce25)) +* local network access on macOS 15 Sequoia ([#630](https://github.com/garethgeorge/backrest/issues/630)) ([0dd360b](https://github.com/garethgeorge/backrest/commit/0dd360b4973b9f60ba706f869a1a6eb883713afd)) +* only log important messages e.g. errors or summary for backup and restore commands ([82f05d8](https://github.com/garethgeorge/backrest/commit/82f05d8b809efb1a7051947cafc75ee75fd2ba5f)) +* provide an option for auto-initializing repos created externally ([#650](https://github.com/garethgeorge/backrest/issues/650)) ([99264b2](https://github.com/garethgeorge/backrest/commit/99264b2469e5f04705036173a2698e6dcef25671)) +* test repo configuration button ([b3cfef1](https://github.com/garethgeorge/backrest/commit/b3cfef14057540bfb0d3d2e67f66d0bbfb6c45dc)) +* test repo configuration button doesn't work ([07a1561](https://github.com/garethgeorge/backrest/commit/07a1561df7aed7265cdfc9561d7bd6a2e10deac2)) +* whitespace at start of path can result in invalid restore target ([47a4b52](https://github.com/garethgeorge/backrest/commit/47a4b522636370ca19d85b7ac5e1019d5b227edc)) + +## [1.7.0](https://github.com/garethgeorge/backrest/compare/v1.6.2...v1.7.0) (2025-01-09) + + +### Features + +* add a "test configuration" button to aid users setting up new repos ([#582](https://github.com/garethgeorge/backrest/issues/582)) ([1bb3cd7](https://github.com/garethgeorge/backrest/commit/1bb3cd70fd8a7eb12df19eaf8f01edb075f34d48)) +* change payload for healthchecks to text ([#607](https://github.com/garethgeorge/backrest/issues/607)) ([a1e3a70](https://github.com/garethgeorge/backrest/commit/a1e3a708eb583c9c7116b9842c0fcd9a04b086af)) +* cont'd windows installer refinements ([#603](https://github.com/garethgeorge/backrest/issues/603)) ([b1b7fb9](https://github.com/garethgeorge/backrest/commit/b1b7fb97077150c7fd5548625c6d790a4006df08)) +* improve repo view layout when backups from multiple-instances are found ([ad5d396](https://github.com/garethgeorge/backrest/commit/ad5d39643ec74a546cb6316da620e3d3bc4c8ae6)) +* initial backend implementation of multihost synchronization ([#562](https://github.com/garethgeorge/backrest/issues/562)) ([a4b4de5](https://github.com/garethgeorge/backrest/commit/a4b4de5152a0437cc2fe88b97fe808d6ef6da75d)) + + +### Bug Fixes + +* avoid ant design url rule as it requires a tld to be present ([#626](https://github.com/garethgeorge/backrest/issues/626)) ([b3402a1](https://github.com/garethgeorge/backrest/commit/b3402a18d2026a2b5998ecdae5a9802f7b3c844a)) +* int overflow in exponential backoff hook error policy ([#619](https://github.com/garethgeorge/backrest/issues/619)) ([1ff69f1](https://github.com/garethgeorge/backrest/commit/1ff69f121ae4f3455e132193dffe6c4a4fa80abd)) +* ogid caching for better insert / update performance ([d9cf79b](https://github.com/garethgeorge/backrest/commit/d9cf79b48a4a1846a709a1d808ade53b17389fcc)) +* rare race condition in etag cache when serving webui ([dbcaa7b](https://github.com/garethgeorge/backrest/commit/dbcaa7b4fb5abe88b9e5cb2ff21f2daad81b4ee5)) +* ui bugs introduced by repo guid migration ([407652c](https://github.com/garethgeorge/backrest/commit/407652c9ef8e8b00e20d76b5fa4b681a32d27a81)) + +## [1.6.2](https://github.com/garethgeorge/backrest/compare/v1.6.1...v1.6.2) (2024-11-26) + + +### Bug Fixes + +* allow 'run command' tasks to proceed in parallel to other repo operations ([3397a01](https://github.com/garethgeorge/backrest/commit/3397a011a3bbbdac2f7299ea4f869cd71b2d0a22)) +* allow for deleting individual operations from the list view ([aa39ead](https://github.com/garethgeorge/backrest/commit/aa39ead0e1f223e7fe7c0ce6fe4dbbc3c3050728)) +* better defaults in add repo / add plan views ([4d7be23](https://github.com/garethgeorge/backrest/commit/4d7be23e8cfd959e93f202eb52c8065446446d07)) +* crash on arm32 device due to bad libc dependency version for sqlite driver ([#559](https://github.com/garethgeorge/backrest/issues/559)) ([e60a4fb](https://github.com/garethgeorge/backrest/commit/e60a4fbcd7b695b03ae5402868ae3c4795cb04c6)) +* garbage collection with more sensible limits grouped by plan/repo ([#555](https://github.com/garethgeorge/backrest/issues/555)) ([492beb2](https://github.com/garethgeorge/backrest/commit/492beb29352ba5e5dc824d35dfaa58eed4422b8a)) +* improve memory pressure from getlogs ([592e4cf](https://github.com/garethgeorge/backrest/commit/592e4cf9dd60eaad1a660c4d69fb4ffea79c98cd)) +* improve windows installer and relocate backrest on Windows to %localappdata%\programs ([#568](https://github.com/garethgeorge/backrest/issues/568)) ([00b0c3e](https://github.com/garethgeorge/backrest/commit/00b0c3e1d256a552aa05a8a90ae05e60d35c5c96)) +* make cancel button more visible for a running operation ([51a6683](https://github.com/garethgeorge/backrest/commit/51a66839ff608fa0e3e60a6a48ca5d490628368e)) +* set etag header to cache webUI source ([0642f4b](https://github.com/garethgeorge/backrest/commit/0642f4b65a11daab379708d7ed813ca8d6a2140f)) +* substantially improve windows installer experience ([#578](https://github.com/garethgeorge/backrest/issues/578)) ([74eb869](https://github.com/garethgeorge/backrest/commit/74eb8692638c04f49004c8312ed57123ea4b5cc2)) +* tray app infers UI port from BACKREST_PORT or --bind-address if available ([c810d27](https://github.com/garethgeorge/backrest/commit/c810d27375c39a9938ad4bde433dfe5997d56bfa)) +* update resticinstaller to use the same binary name across versions and to use system restic install when possible ([5fea5fd](https://github.com/garethgeorge/backrest/commit/5fea5fdefdc2bad7fccb1f0cc0ea57fbe79bbcbb)) +* use command mode when executing powershell scripts on windows ([#569](https://github.com/garethgeorge/backrest/issues/569)) ([57f9aeb](https://github.com/garethgeorge/backrest/commit/57f9aeb72a6db240824998cff91c0921c68a336a)) +* webui may duplicate elements in a multi-instance repo ([bf77bab](https://github.com/garethgeorge/backrest/commit/bf77baba06c7296ade830e10238f1a02d0cea95c)) + +## [1.6.1](https://github.com/garethgeorge/backrest/compare/v1.6.0...v1.6.1) (2024-10-20) + + +### Bug Fixes + +* login form has no background ([4fc28d6](https://github.com/garethgeorge/backrest/commit/4fc28d68a60721d333be96df2030ce53b04fbf55)) +* stats operation occasionally runs twice in a row ([36543c6](https://github.com/garethgeorge/backrest/commit/36543c681ac1f138e4d207f96c143b1d1ffd84fe)) +* tarlog migration fails on new installs ([5617f3f](https://github.com/garethgeorge/backrest/commit/5617f3fbe2aa5278c2b8b1903997980a9e2e16b0)) + +## [1.6.0](https://github.com/garethgeorge/backrest/compare/v1.5.1...v1.6.0) (2024-10-20) + + +### Features + +* add a summary dashboard as the "main view" when backrest opens ([#518](https://github.com/garethgeorge/backrest/issues/518)) ([4b3c7e5](https://github.com/garethgeorge/backrest/commit/4b3c7e53d5b8110c179c486c3423ef9ff72feb8f)) +* add watchdog thread to reschedule tasks when system time changes ([66a5241](https://github.com/garethgeorge/backrest/commit/66a5241de8cf410d0766d7e70de9b8f87e6aaddd)) +* initial support for healthchecks.io notifications ([#480](https://github.com/garethgeorge/backrest/issues/480)) ([f6ee51f](https://github.com/garethgeorge/backrest/commit/f6ee51fce509808d8dde3d2af21d10994db381ca)) +* migrate oplog history from bbolt to sqlite store ([#515](https://github.com/garethgeorge/backrest/issues/515)) ([0806eb9](https://github.com/garethgeorge/backrest/commit/0806eb95a044fd5f1da44aff7713b0ca21f7aee5)) +* support --skip-if-unchanged ([afcecae](https://github.com/garethgeorge/backrest/commit/afcecaeb3064782788a4ff41fc31a541d93e844f)) +* track long running generic commands in the oplog ([#516](https://github.com/garethgeorge/backrest/issues/516)) ([28c3172](https://github.com/garethgeorge/backrest/commit/28c31720f249763e2baee43671475c128d17b020)) +* use react-router to enable linking to webUI pages ([#522](https://github.com/garethgeorge/backrest/issues/522)) ([fff3dbd](https://github.com/garethgeorge/backrest/commit/fff3dbd299163b916ae0c6819c9c0170e2e77dd9)) +* use sqlite logstore ([#514](https://github.com/garethgeorge/backrest/issues/514)) ([4d557a1](https://github.com/garethgeorge/backrest/commit/4d557a1146b064ee41d74c80667adcd78ed4240c)) + + +### Bug Fixes + +* expand env vars in flags i.e. of the form ${MY_ENV_VAR} ([d7704cf](https://github.com/garethgeorge/backrest/commit/d7704cf057989af4ed2f03e81e46a6a924f833cd)) +* gorelaeser docker image builds for armv6 and armv7 ([4fa30e3](https://github.com/garethgeorge/backrest/commit/4fa30e3f7ee7456d2bdf4afccb47918d01bdd32e)) +* plan/repo settings button hard to click ([ec89cfd](https://github.com/garethgeorge/backrest/commit/ec89cfde518e3c38697e6421fa7e1bca31040602)) + +## [1.5.1](https://github.com/garethgeorge/backrest/compare/v1.5.0...v1.5.1) (2024-09-18) + + +### Bug Fixes + +* **docs:** correct minor spelling and grammar errors ([#479](https://github.com/garethgeorge/backrest/issues/479)) ([df55681](https://github.com/garethgeorge/backrest/commit/df5568132b56d38f0ce155e546ff110a943ad87a)) +* prunepolicy.max_unused_percent should allow decimal values ([3056203](https://github.com/garethgeorge/backrest/commit/3056203127b4ced26e69da2a7540d4b139dcd8e9)) +* stats panel can fail to load when an incomplete operation is in the log ([d59c6fc](https://github.com/garethgeorge/backrest/commit/d59c6fc1bed06718c49fc87bfc5bf143a10ac5ed)) +* update to newest restic bugfix release 0.17.1 ([d2650fd](https://github.com/garethgeorge/backrest/commit/d2650fdd591f2bdb08dce8fe55afaba0a5659e31)) +* windows installation for restic 0.17.1 ([#474](https://github.com/garethgeorge/backrest/issues/474)) ([4da9d89](https://github.com/garethgeorge/backrest/commit/4da9d89749fd1bdfd9701c8efb83b69a7eef3395)) + +## [1.5.0](https://github.com/garethgeorge/backrest/compare/v1.4.0...v1.5.0) (2024-09-10) + + +### Features + +* add prometheus metrics ([#459](https://github.com/garethgeorge/backrest/issues/459)) ([daacf28](https://github.com/garethgeorge/backrest/commit/daacf28699c18b27256cb4bf2eb3d9caf94a5ce8)) +* compact the scheduling UI and use an enum for clock configuration ([#452](https://github.com/garethgeorge/backrest/issues/452)) ([9205da1](https://github.com/garethgeorge/backrest/commit/9205da1d2380410d1ccc4507008f28d4fa60dd32)) +* implement 'on error retry' policy ([#428](https://github.com/garethgeorge/backrest/issues/428)) ([038bc87](https://github.com/garethgeorge/backrest/commit/038bc87070361ff3b7d9a90c075787e9ff3948f7)) +* implement scheduling relative to last task execution ([#439](https://github.com/garethgeorge/backrest/issues/439)) ([6ed1280](https://github.com/garethgeorge/backrest/commit/6ed1280869bf42d1901ca09a5cc6b316a1cd8394)) +* support live logrefs for in-progress operations ([#456](https://github.com/garethgeorge/backrest/issues/456)) ([bfaad8b](https://github.com/garethgeorge/backrest/commit/bfaad8b69e95e13006d3f64e6daa956dc060833c)) + + +### Bug Fixes + +* apply oplog migrations correctly using new storage interface ([491a6a6](https://github.com/garethgeorge/backrest/commit/491a6a67254e40167b6937f6844123de704d5182)) +* backrest can erroneously show 'forget snapshot' button for restore entries ([bfde425](https://github.com/garethgeorge/backrest/commit/bfde425c2d03b0e4dc7c19381cb604dcba9d36e3)) +* broken refresh and sizing for mobile view in operation tree ([0d01c5c](https://github.com/garethgeorge/backrest/commit/0d01c5c31773de996465574e77bc90fa64586e59)) +* bugs in displaying repo / plan / activity status ([cceda4f](https://github.com/garethgeorge/backrest/commit/cceda4fdea5f6c2072e8641d33fffe160613dcf7)) +* double display of snapshot ID for 'Snapshots' in operation tree ([80dbe91](https://github.com/garethgeorge/backrest/commit/80dbe91729efebe88d4ad8e9c4160d48254d0fc1)) +* hide system operations in tree view ([8c1cf79](https://github.com/garethgeorge/backrest/commit/8c1cf791bbc2a5fc0ff279f9ba52d372c123f2d2)) +* misc bugs in restore operation view and activity bar view ([656ac9e](https://github.com/garethgeorge/backrest/commit/656ac9e1b2f2ce82f5afd4a20a729b710d19c541)) +* misc bugs related to new logref support ([97e3f03](https://github.com/garethgeorge/backrest/commit/97e3f03b78d9af644aaa9f4b2e4882514c85025a)) +* misc logging improvements ([1879ddf](https://github.com/garethgeorge/backrest/commit/1879ddfa7991f44bd54d3de9d14d7b7c03472c78)) +* new config validations make it harder to lock yourself out of backrest ([c419861](https://github.com/garethgeorge/backrest/commit/c4198619aa93fa216b9b2744cb7e4214e23c6ac6)) +* reformat tags row in operation list ([0eb560d](https://github.com/garethgeorge/backrest/commit/0eb560ddfb46f33d8404d0e7ac200d7574f64797)) +* remove migrations for fields that have been since backrest 1.0.0 ([#453](https://github.com/garethgeorge/backrest/issues/453)) ([546482f](https://github.com/garethgeorge/backrest/commit/546482f11533668b58d5f5eead581a053b19c28d)) +* restic cli commands through 'run command' are cancelled when closing dialogue ([bb00afa](https://github.com/garethgeorge/backrest/commit/bb00afa899b17c23f6375a5ee23d3c5354f5df4d)) +* simplify auth handling ([6894128](https://github.com/garethgeorge/backrest/commit/6894128d90c1d50c9da53276e4dd6b37c5357402)) +* test fixes for windows file restore ([44585ed](https://github.com/garethgeorge/backrest/commit/44585ede613b87189c38f5cd456a109e653cdf64)) +* UI quality of life improvements ([cc173aa](https://github.com/garethgeorge/backrest/commit/cc173aa7b1b9dda10cfb14ca179c9701d15f22f5)) +* use 'restic restore <snapshot id>:' for restore operations ([af09e47](https://github.com/garethgeorge/backrest/commit/af09e47cdda921eb11cab970939740adb1612af4)) +* write debug-level logs to data dir on all platforms ([a9eb786](https://github.com/garethgeorge/backrest/commit/a9eb786db90f977984b13c3bda7f764d6dadbbef)) + +## [1.4.0](https://github.com/garethgeorge/backrest/compare/v1.3.1...v1.4.0) (2024-08-15) + + +### Features + +* accept up to 2 decimals of precision for check % and prune % policies ([5374273](https://github.com/garethgeorge/backrest/commit/53742736f9dec217527ad50caed9a488da39ad45)) +* add UI support for new summary details introduced in restic 0.17.0 ([4859e52](https://github.com/garethgeorge/backrest/commit/4859e528c73853d4597c5ef54d3054406a5c7e44)) +* start tracking snapshot summary fields introduced in restic 0.17.0 ([505765d](https://github.com/garethgeorge/backrest/commit/505765dff978c5ecabe1986907b4c4c0c5112daf)) +* update to restic 0.17.0 ([#416](https://github.com/garethgeorge/backrest/issues/416)) ([500f2ee](https://github.com/garethgeorge/backrest/commit/500f2ee6c0d8bcf65a37462d3d03452cd9dff817)) + + +### Bug Fixes + +* activitybar does not reset correctly when an in-progress operation is deleted ([244fe7e](https://github.com/garethgeorge/backrest/commit/244fe7edd203b566709dc7f14091865bc9ed6700)) +* add condition_snapshot_success to .EventName ([#410](https://github.com/garethgeorge/backrest/issues/410)) ([c45f0f3](https://github.com/garethgeorge/backrest/commit/c45f0f3c668df44ba82e0d6faf73cfd8f39f0c2a)) +* backrest should only initialize repos explicitly added through WebUI ([62a97a3](https://github.com/garethgeorge/backrest/commit/62a97a335df3858a53eba34e7b7c0f69e3875d88)) +* forget snapshot by ID should not require a plan ([49e46b0](https://github.com/garethgeorge/backrest/commit/49e46b04a06eb75829df2f97726d850749e29b74)) +* hide cron options for hours/minutes/days of week for infrequent schedules ([7c091e0](https://github.com/garethgeorge/backrest/commit/7c091e05973addaa35850774320f5e49fe016437)) +* improve debug output when trying to configure a new repo ([11b3e99](https://github.com/garethgeorge/backrest/commit/11b3e9915211c8c4a06f9f6f0c30f07f005a0036)) +* possible race condition leading to rare panic in GetOperationEvents ([f250adf](https://github.com/garethgeorge/backrest/commit/f250adf4a025dcb64cb569a8cb26fa0443b56fae)) +* run list snapshots after updating repo config or adding new repo ([48626b9](https://github.com/garethgeorge/backrest/commit/48626b923ea5022d9c4f2075d5c2c1ec19089499)) +* use addrepo RPC to apply validations when updating repo config ([a67c29b](https://github.com/garethgeorge/backrest/commit/a67c29b57ac7154bda87a7a460af26adacf6d11b)) + +## [1.3.1](https://github.com/garethgeorge/backrest/compare/v1.3.0...v1.3.1) (2024-07-12) + + +### Bug Fixes + +* add docker-cli to alpine backrest image ([b6f9129](https://github.com/garethgeorge/backrest/commit/b6f9129d3042a3785106ecd24801a55b80b38146)) +* add major and major.minor semantic versioned docker releases ([8db2578](https://github.com/garethgeorge/backrest/commit/8db2578b95d50dcd4abaac851c8a1a5b6e9bf15c)) +* plan _system_ not found bug when running health operations ([c19665a](https://github.com/garethgeorge/backrest/commit/c19665ab063a32e2cb0ca73a4e0eaa4cee793601)) + +## [1.3.0](https://github.com/garethgeorge/backrest/compare/v1.2.1...v1.3.0) (2024-07-11) + + +### Features + +* improve hook UX and execution model ([#357](https://github.com/garethgeorge/backrest/issues/357)) ([4d0d13e](https://github.com/garethgeorge/backrest/commit/4d0d13e39802fcf18186723372608d96b9bd58b0)) + + +### Bug Fixes + +* cannot run path relative executable errors on Windows ([c3ec9ee](https://github.com/garethgeorge/backrest/commit/c3ec9eeb4b5aa37e66ad115528b6708d438e9459)) +* improve handling of restore operations ([620caed](https://github.com/garethgeorge/backrest/commit/620caed7e3570aa7f7cb5f7279c8b6bb277d95fc)) +* operation tree key conflicts ([2dc5595](https://github.com/garethgeorge/backrest/commit/2dc55951d7047e395c0b770bc8e4d1a80ffd32d7)) + +## [1.2.1](https://github.com/garethgeorge/backrest/compare/v1.2.0...v1.2.1) (2024-07-02) + + +### Bug Fixes + +* AddPlanModal and AddRepoModal should only be closeable explicitly ([15f92fc](https://github.com/garethgeorge/backrest/commit/15f92fcd901da8c06ebd94576b09879e68bf5bc5)) +* disable sorting for excludes and iexcludes ([d7425b5](https://github.com/garethgeorge/backrest/commit/d7425b589376595999d3e3f401bb7ef77ffde991)) +* github actions release flow for windows installers ([90e0656](https://github.com/garethgeorge/backrest/commit/90e0656fc41a2b90ee24d598023ccc6996a64b9c)) +* make instance ID required field ([7c8ded2](https://github.com/garethgeorge/backrest/commit/7c8ded2fcc4b597e21c24f451e02cc14ba9a015c)) +* operation tree UI bugs ([76ce3c1](https://github.com/garethgeorge/backrest/commit/76ce3c177b6a92c105c874e459bd57e1122b5ce8)) +* restore always uses ~/Downloads path ([955771e](https://github.com/garethgeorge/backrest/commit/955771e1cc6bb7b143ef5c51ef9e1e09509f76b1)) + +## [1.1.0](https://github.com/garethgeorge/backrest/compare/v1.0.0...v1.1.0) (2024-06-01) + + +### Features + +* add windows installer and tray app ([#294](https://github.com/garethgeorge/backrest/issues/294)) ([8a7543c](https://github.com/garethgeorge/backrest/commit/8a7543c7bf7f245d87fa079c477c50b333dfba37)) +* support nice/ionice as a repo setting ([#309](https://github.com/garethgeorge/backrest/issues/309)) ([0c9f366](https://github.com/garethgeorge/backrest/commit/0c9f366e439b57007259a2ca305ac00733638566)) +* support restic check operation ([#303](https://github.com/garethgeorge/backrest/issues/303)) ([ce42f68](https://github.com/garethgeorge/backrest/commit/ce42f68d0d563defabbaafce63313fcf3835d2dd)) + + +### Bug Fixes + +* collection of ui refresh timing bugs ([b218bc9](https://github.com/garethgeorge/backrest/commit/b218bc9409bf4a6c70da06e1f98760ff520afc37)) +* improve prune and check scheduling in new repos ([c58055e](https://github.com/garethgeorge/backrest/commit/c58055ec91ccc9a8afc5d3ff402f68da00a04e66)) +* release workflow ([290d018](https://github.com/garethgeorge/backrest/commit/290d018c7585a4032b5f5d7a26f06e4d74f8b5cb)) +* snapshot browser on Windows ([19ed611](https://github.com/garethgeorge/backrest/commit/19ed611477186af2702fb7ba403b0bac45ccc4aa)) +* UI refresh timing bugs ([ba005ae](https://github.com/garethgeorge/backrest/commit/ba005aee0beb0105948901330e9ab7f7290eec92)) + +## [1.0.0](https://github.com/garethgeorge/backrest/compare/v0.17.2...v1.0.0) (2024-05-20) + + +### ⚠ BREAKING CHANGES + +* redefine hostname as a required property that maps to --host ([#256](https://github.com/garethgeorge/backrest/issues/256)) + +### Features + +* add CONDITION_SNAPSHOT_WARNING hook triggered by any warning status at the completion of a snapshot ([f0ee20f](https://github.com/garethgeorge/backrest/commit/f0ee20f53de58e0a0a0a63137e203161d8acce4d)) +* add download link to create a zip archive of restored files ([a75a5c2](https://github.com/garethgeorge/backrest/commit/a75a5c2297df4eb89235a54efd38d9539b7c15e5)) +* add force kill signal handler that dumps stacks ([386f46a](https://github.com/garethgeorge/backrest/commit/386f46a090e6df28f28cbca15d992ce4ad6d5dd5)) +* add seek support to join iterator for better performance ([802146a](https://github.com/garethgeorge/backrest/commit/802146a6c023779d6e5e0879994ec7dc5479e304)) +* ensure instance ID is set for all operations ([65d4a1d](https://github.com/garethgeorge/backrest/commit/65d4a1df0e9e717f5f88d7c5bec37f18d877b876)) +* implement 'run command' button to execute arbitrary restic commands in a repo ([fbad981](https://github.com/garethgeorge/backrest/commit/fbad981a1d3ae75c1eeebf9fd3bf4cef4f72b4c4)) +* improve support for instance ID tag ([be0cdd5](https://github.com/garethgeorge/backrest/commit/be0cdd59be270e0393dc4d587bfa708c610ac0a5)) +* keep a rolling backup of the last 10 config versions ([1a053f2](https://github.com/garethgeorge/backrest/commit/1a053f274846e822ecfd3c76e0d1b4860fada58a)) +* overhaul task interface and introduce 'flow ID' for simpler grouping of operations ([#253](https://github.com/garethgeorge/backrest/issues/253)) ([7a10bdc](https://github.com/garethgeorge/backrest/commit/7a10bdca7b00f337a2c85780861e479b7aa35cb5)) +* redefine hostname as a required property that maps to --host ([#256](https://github.com/garethgeorge/backrest/issues/256)) ([4847010](https://github.com/garethgeorge/backrest/commit/484701007ff2f7f80fff308827b1af456a78cbb9)) +* support env variable substitution e.g. FOO=${MY_FOO_VAR} ([8448f4c](https://github.com/garethgeorge/backrest/commit/8448f4cc3aebd1b481fc695c2aa0d02e18689a20)) +* unified scheduling model ([#282](https://github.com/garethgeorge/backrest/issues/282)) ([531cd28](https://github.com/garethgeorge/backrest/commit/531cd286d87c8004b95bfd9b4512dffccc6d500d)) +* update snapshot management to track and filter on instance ID, migrate existing snapshots ([5a996d7](https://github.com/garethgeorge/backrest/commit/5a996d74b06dcf6c1439cac9134ec51ba7167c15)) +* validate plan ID and repo ID ([f314c7c](https://github.com/garethgeorge/backrest/commit/f314c7cced2db23a4008622c97a27697c832c664)) + + +### Bug Fixes + +* add virtual root node to snapshot browser ([6045c87](https://github.com/garethgeorge/backrest/commit/6045c87cdf5a68afd81203602ee5827eda5af8e7)) +* additional tooltips for add plan modal ([fcdf07d](https://github.com/garethgeorge/backrest/commit/fcdf07da6c330aed7fea017835cbbf56679b3749)) +* adjust task priorities ([756e64a](https://github.com/garethgeorge/backrest/commit/756e64a2002aead213d67c8d37d851688af51168)) +* center-right align settings icons for plans/repos ([982e2fb](https://github.com/garethgeorge/backrest/commit/982e2fb2cd84fe193a4b37bda8c21f75c8eb3382)) +* concurrency issues in run command handler ([411a4fb](https://github.com/garethgeorge/backrest/commit/411a4fb6f00fd46f1fbdb0b8e3a971d016a6e0f8)) +* date formatting ([b341146](https://github.com/garethgeorge/backrest/commit/b341146fce40ee8bdaf771c4c5269160198b6386)) +* downgrade omission of 'instance' field from an error to a warning ([6ae82f7](https://github.com/garethgeorge/backrest/commit/6ae82f70d456c05b3ad0ab01e901be8bd01bb9eb)) +* error formatting for repo init ([1a3ace9](https://github.com/garethgeorge/backrest/commit/1a3ace90141a48e949c6c796fa8445de134baa98)) +* hide successful hook executions in the backup view ([65bb8ef](https://github.com/garethgeorge/backrest/commit/65bb8ef14b77cfe07c2db26e0fcc8e0bbc1a9287)) +* improve cmd error formatting now that logs are available for all operations ([6eb704f](https://github.com/garethgeorge/backrest/commit/6eb704f07bfae1cfc25208bc1a20908d229f344e)) +* improve concurrency handling in RunCommand ([07b0950](https://github.com/garethgeorge/backrest/commit/07b09502b9554386afa7bd4c5487f9b8da3a59bb)) +* improve download speeds for restored files ([eb07931](https://github.com/garethgeorge/backrest/commit/eb079317c05946fb74a74e59592940ada9eef4ea)) +* install.sh was calling systemctl on Darwin ([#260](https://github.com/garethgeorge/backrest/issues/260)) ([f6d5837](https://github.com/garethgeorge/backrest/commit/f6d58376b76707de36d851808812d6b3384e2ca9)) +* minor bugs and tweak log rotation history to 14 days ([ad9a770](https://github.com/garethgeorge/backrest/commit/ad9a77029ce07a5bb7da2738b108d0f93cb57440)) +* miscellaneous bug fixes ([df4be0f](https://github.com/garethgeorge/backrest/commit/df4be0f7bc014a3862f14fcf79cffc53f45c6ea0)) +* prompt for user action to set an instance ID on upgrade ([294864f](https://github.com/garethgeorge/backrest/commit/294864fe433302571ba9ff9eb7c2dd475fa1c560)) +* rebase stats panel onto a better chart library ([b22028e](https://github.com/garethgeorge/backrest/commit/b22028eb4f185be96ff4407fccafa2d1cdf491a1)) +* reserve IDs starting and ending with '__' for internal use ([711064f](https://github.com/garethgeorge/backrest/commit/711064fb0017830bc148643617ca8da5aa0add41)) +* retention policy display may show default values for some fields ([9d6c1ba](https://github.com/garethgeorge/backrest/commit/9d6c1baf87c31b7a2cfb633fdd228d58021f7b0f)) +* run stats after every prune operation ([7fce593](https://github.com/garethgeorge/backrest/commit/7fce59311d531cb9058965cde780f8930cd98a9b)) +* schedule view bug ([0764804](https://github.com/garethgeorge/backrest/commit/0764804ea558df6edd5e65ca1ea9c843a75fc147)) +* secure download URLs when downloading tar archive of exported files ([a30d5ef](https://github.com/garethgeorge/backrest/commit/a30d5efe1c354dd6f6c91d3b1465a244077e1e47)) +* UI fixes for restore row and settings modal ([e9d6cbe](https://github.com/garethgeorge/backrest/commit/e9d6cbeaff03675928e036461a999cb4bde64e54)) +* use int64 for large values in structs for compatibility with 32bit devices ([#250](https://github.com/garethgeorge/backrest/issues/250)) ([84b4b68](https://github.com/garethgeorge/backrest/commit/84b4b68760ded53d9bda2fbc992646f309094f52)) +* use locale to properly format time ([89a49c1](https://github.com/garethgeorge/backrest/commit/89a49c1fa7c6cafedef30bdf695e76920e2c690c)) + +## [0.17.2](https://github.com/garethgeorge/backrest/compare/v0.17.1...v0.17.2) (2024-04-18) + +### Bug Fixes + +- add tini to docker images to reap rclone processes left behind by restic ([6408518](https://github.com/garethgeorge/backrest/commit/6408518582fb2a1b529f5c9fb0c595df230f3df6)) +- armv7 support for docker releases ([ec39533](https://github.com/garethgeorge/backrest/commit/ec39533e4cddf2f0354ec3fcb4c52ba37a9b00ec)) +- bug in new task queue implementation ([5d6074e](https://github.com/garethgeorge/backrest/commit/5d6074eb296e6737f1959fba913c67e09e60ef47)) +- improve restic pkg's output handling and buffering ([aacdf9b](https://github.com/garethgeorge/backrest/commit/aacdf9b7cd529a6f677cd7f1d9ed2fbbcadc9b8a)) +- Linux ./install.sh script fails when used for updating backrest ([#226](https://github.com/garethgeorge/backrest/issues/226)) ([be09303](https://github.com/garethgeorge/backrest/commit/be0930368b83ba8f159b28bc286300c56bd6a3a3)) +- use new orchestrator queue ([4a81889](https://github.com/garethgeorge/backrest/commit/4a81889d810d409ed42fcf07a0fa6a4ac97db72b)) + +## [0.17.1](https://github.com/garethgeorge/backrest/compare/v0.17.0...v0.17.1) (2024-04-12) + +### Bug Fixes + +- revert orchestrator changes ([07cffcb](https://github.com/garethgeorge/backrest/commit/07cffcb5d8dc018631fcd0d1f98cc01553a6574e)) + +## [0.17.0](https://github.com/garethgeorge/backrest/compare/v0.16.0...v0.17.0) (2024-04-12) + +### Features + +- add a Bash script to help Linux user manage Backrest ([#187](https://github.com/garethgeorge/backrest/issues/187)) ([d78bcfa](https://github.com/garethgeorge/backrest/commit/d78bcfab845a86523868a91fe200b2a3c4c07e07)) +- allow hook exit codes to control backup execution (e.g fail, skip, etc) ([c4ae5b3](https://github.com/garethgeorge/backrest/commit/c4ae5b3f2257d6c04ed08188cfc509023137b460)) +- release backrest as a homebrew tap ([16a7d0e](https://github.com/garethgeorge/backrest/commit/16a7d0e95ae51c9f86e2d38e2c494b324245a9db)) +- use amd64 restic for arm64 Windows ([#201](https://github.com/garethgeorge/backrest/issues/201)) ([3770966](https://github.com/garethgeorge/backrest/commit/3770966111f096c84b4702e6639397e8efab93a7)) +- use new task queue implementation in orchestrator ([1d04898](https://github.com/garethgeorge/backrest/commit/1d0489847e6fee5baed807117379738aceca4a2d)) + +### Bug Fixes + +- address minor data race in command output handling and enable --race in coverage ([3223138](https://github.com/garethgeorge/backrest/commit/32231385ed20c0dccda12361eaac7cc088ec15a0)) +- bugs in refactored task queue and improved coverage ([834b74f](https://github.com/garethgeorge/backrest/commit/834b74f0f3eec42055d1af6ecfe34d448f71d97b)) +- cannot set retention policy buckets to 0 ([7e9bf15](https://github.com/garethgeorge/backrest/commit/7e9bf15976006c7f3ff96948d2b2c291737c9e88)) +- **css:** fixing overflow issue ([#191](https://github.com/garethgeorge/backrest/issues/191)) ([1d9e43e](https://github.com/garethgeorge/backrest/commit/1d9e43e49b21adc4ed8ce1ec96199084981d709a)) +- default BACKREST_PORT to 127.0.0.1:9898 (localhost only) when using install.sh ([eb07230](https://github.com/garethgeorge/backrest/commit/eb07230cc0843643406fa44ca21c3a138baced77)) +- handle backpressure correctly in event stream ([4e2bf1f](https://github.com/garethgeorge/backrest/commit/4e2bf1f76c4d35d61fc48111baaa33b7b7a8c133)) +- improve tooltips on AddRepoModal ([e2be189](https://github.com/garethgeorge/backrest/commit/e2be189f9e4bb617a69e4b9c15da3d1920549349)) +- include ioutil helpers ([88a926b](https://github.com/garethgeorge/backrest/commit/88a926b0a3a52efb82da5df3423a001ed140639c)) +- limit cmd log length to 32KB per operation ([92d52be](https://github.com/garethgeorge/backrest/commit/92d52bed8e84d6cd8dd331a1fa52a6e2d30cb7a7)) +- misc UI and backend bug fixes ([e96f403](https://github.com/garethgeorge/backrest/commit/e96f4036df6849650d6e378c9a175fef86b2962b)) +- refactor priority ordered task queue implementation ([8b9280e](https://github.com/garethgeorge/backrest/commit/8b9280ed57b84b7da814e285542c57b7c14209ae)) +- spawn goroutine to update oplog with progress during backup/restore ([eab1c1b](https://github.com/garethgeorge/backrest/commit/eab1c1bffe2a1aec6afa9e054278ff98ca3047cf)) +- use C:\Program Files\backrest on both x64 and 32-bit ([#200](https://github.com/garethgeorge/backrest/issues/200)) ([7b0d3aa](https://github.com/garethgeorge/backrest/commit/7b0d3aa1be7bc93363b00154d09502b4e4e63ba5)) + +## [0.16.0](https://github.com/garethgeorge/backrest/compare/v0.15.1...v0.16.0) (2024-03-30) + +### Features + +- allow disabling authentication ([8429174](https://github.com/garethgeorge/backrest/commit/84291746af5fc863f90bcf7ae9ba5a2d3ca26cdd)) +- improve consistency of restic command execution and output capture ([16e22aa](https://github.com/garethgeorge/backrest/commit/16e22aa623c5a0a6e6b0e6df12a8e3d09c2ff31f)) +- improve observability by exposing restic command logs in UI ([eeb8c8e](https://github.com/garethgeorge/backrest/commit/eeb8c8e6b377f96c0c39bd2b169b86986933d570)) +- make hostname configurable in settings panel ([2e4e3cf](https://github.com/garethgeorge/backrest/commit/2e4e3cf9c78cac587a3a40635ec068726b3f4d2d)) +- sort lists in configuration ([6f330ac](https://github.com/garethgeorge/backrest/commit/6f330ac37b8ce621fbe82594c41d6f5091f03dfd)) +- support shoutrrr notification service ([fa6407c](https://github.com/garethgeorge/backrest/commit/fa6407cac25ed8f0a32cc9ed5fdd8454bc9abbe5)) +- switch alpine as the default base image for docker releases ([7425c9b](https://github.com/garethgeorge/backrest/commit/7425c9bb0e08cf650e596ae43a736507313e3f2f)) +- update macos install script to set PATH env var for use with rclone ([8cf43f2](https://github.com/garethgeorge/backrest/commit/8cf43f28921ef7182f1c655fa82470e74698d3ce)) + +### Bug Fixes + +- add new logs to orchestrator and increase clock change polling to every 5 minutes ([5b7e2b0](https://github.com/garethgeorge/backrest/commit/5b7e2b080d31a2f77a5f9b6737dfbb84cfb63cce)) +- api path relative to UI serving location to support reverse proxies with prefix stripping ([ac7f24e](https://github.com/garethgeorge/backrest/commit/ac7f24ed04679ed6cc3ea779325c0e0b49c9f526)) +- cleanup spacing and hook titles in AddRepoModal and AddPlanModal ([c32874c](https://github.com/garethgeorge/backrest/commit/c32874c1d6fc8292a2fb91f0b22c7146083bc468)) +- correctly auto-expand first 5 backups when opening plan/repo ([d7ca35b](https://github.com/garethgeorge/backrest/commit/d7ca35b66f61c12360905e98b775e3256210176e)) +- include error messages in restic logs ([b68f7c6](https://github.com/garethgeorge/backrest/commit/b68f7c69138d516f84f9fca3040003604bff24e6)) +- include restic binary in alpine and scratch docker images ([f7bd9f7](https://github.com/garethgeorge/backrest/commit/f7bd9f7d0a9c62baedd1a341eb76e836fb00cfa5)) +- incorrectly indicate AM/PM in formatted date strings ([5d34e0b](https://github.com/garethgeorge/backrest/commit/5d34e0bfb5cffd44d971b0e1052574fe640049e7)) +- make notification title optional on discord notifications ([e8bbe2c](https://github.com/garethgeorge/backrest/commit/e8bbe2c8f509de67181750f8451fae841b3fa195)) +- make tree view the default panel for repo overview ([3f9c9f4](https://github.com/garethgeorge/backrest/commit/3f9c9f4ff8bea0f79b03222609d7c302e241bab2)) +- tasks duplicated when config is updated during a running operation ([035684c](https://github.com/garethgeorge/backrest/commit/035684ca343b47dfb3f131c89e15f06e8155f550)) + +## [0.15.1](https://github.com/garethgeorge/backrest/compare/v0.15.0...v0.15.1) (2024-03-19) + +### Bug Fixes + +- forget operations failing with new retention policy format ([0a059bb](https://github.com/garethgeorge/backrest/commit/0a059bbb39ea6d5f6f989cc4a4541ec8aedbc071)) + +## [0.15.0](https://github.com/garethgeorge/backrest/compare/v0.13.0...v0.15.0) (2024-03-19) + +### Features + +- add 'compute stats' button to refresh stats on repo view ([1f42b6a](https://github.com/garethgeorge/backrest/commit/1f42b6ab4e0313bbb12e6bc22b561d7544504644)) +- add option to disable scheduled execution of a plan ([aea74c5](https://github.com/garethgeorge/backrest/commit/aea74c51c0fb3908ece57f813c9ae6190e1fd46b)) +- add release artifacts for arm32 ([a737371](https://github.com/garethgeorge/backrest/commit/a737371ed559f5b65e734b0d97c44dcb2749ce53)) +- automatically remove Apples quarantine flag ([#155](https://github.com/garethgeorge/backrest/issues/155)) ([3e76beb](https://github.com/garethgeorge/backrest/commit/3e76bebd054eb7bfc9f8da4681459b863ae50c55)) +- check for basic auth ([#110](https://github.com/garethgeorge/backrest/issues/110)) ([#129](https://github.com/garethgeorge/backrest/issues/129)) ([871c54f](https://github.com/garethgeorge/backrest/commit/871c54f35f8651632714ca7d3a3ab0e809549b51)) +- improved stats visualization with graphs and cleanup operation filtering ([5b362cc](https://github.com/garethgeorge/backrest/commit/5b362ccbb45e59954dad574b93848195d45b55ef)) +- pass through all env variables from parent process to restic ([24afd51](https://github.com/garethgeorge/backrest/commit/24afd514ad80f542e6e1862d1c42195c6fbe1b47)) +- support flag overrides for 'restic backup' in plan configuration ([56f5e40](https://github.com/garethgeorge/backrest/commit/56f5e405037a6309a3d1299356b363cd84281aef)) +- use disambiguated retention policy format ([5a5a229](https://github.com/garethgeorge/backrest/commit/5a5a229f456bf3d4d34cb4751c2a2ff3b6907511)) + +### Bug Fixes + +- alpine linux Dockerfile and add openssh ([3cb9d27](https://github.com/garethgeorge/backrest/commit/3cb9d2717c1bda7bb7ed4e029ac938c851b9f664)) +- backrest shows hidden operations in list view ([c013f06](https://github.com/garethgeorge/backrest/commit/c013f069ff5eab6177d2bde373f23efe34b1aa8d)) +- BackupInfoCollector handling of filtered events ([f1e4619](https://github.com/garethgeorge/backrest/commit/f1e4619e9d98416289fb0ee51d56ff48e163b85d)) +- bugs in env var validation and form field handling ([7e909c4](https://github.com/garethgeorge/backrest/commit/7e909c4a96b053e8093f3b4f3d26c46b1c618947)) +- compression progress ratio should be float64 ([1759b5d](https://github.com/garethgeorge/backrest/commit/1759b5dc55ab17a1c76d47adee7f4e21f7ef09f5)) +- handle timezone correctly with tzdata package on alpine ([0e94f30](https://github.com/garethgeorge/backrest/commit/0e94f308cde40059f9c4104ed21f8c701a349c57)) +- install rclone with apk for alpine image ([#138](https://github.com/garethgeorge/backrest/issues/138)) ([79715a9](https://github.com/garethgeorge/backrest/commit/79715a97b34af60ca90894065d89c9ae603f0a59)) +- proper display of retention policy ([38ff5fe](https://github.com/garethgeorge/backrest/commit/38ff5fecee3ff3cdff5c7ccecb48e600eb714511)) +- properly parse repo flags ([348ec46](https://github.com/garethgeorge/backrest/commit/348ec4690cab74c3089f2be33d889df3002a5a97)) +- stat operation interval for long running repos ([f2477ab](https://github.com/garethgeorge/backrest/commit/f2477ab06cbe571723cd7290e06e8890747f81aa)) +- stats chart titles invisible on light color theme ([746fd9c](https://github.com/garethgeorge/backrest/commit/746fd9cf768f0c87a25f0015bd20289716b08604)) + +### Miscellaneous Chores + +- bump version to 0.15.0 ([db4b76d](https://github.com/garethgeorge/backrest/commit/db4b76de8ed09c9eda6216e8dfe041518f5bbfc5)) + +## [0.13.0](https://github.com/garethgeorge/backrest/compare/v0.12.2...v0.13.0) (2024-02-21) + +### Features + +- add case insensitive excludes (iexcludes) ([#108](https://github.com/garethgeorge/backrest/issues/108)) ([bf6fb7e](https://github.com/garethgeorge/backrest/commit/bf6fb7e71402590961271e91ad6da63db27ff5ad)) +- add flags to configure backrest options e.g. --config-file, --data-dir, --restic-cmd, --bind-address ([41ddc8e](https://github.com/garethgeorge/backrest/commit/41ddc8e1a9d5501a92498c8cf3c72625bd181f8a)) +- add opt-in auto-unlock feature to remove locks on forget and prune ([#107](https://github.com/garethgeorge/backrest/issues/107)) ([c1ee33f](https://github.com/garethgeorge/backrest/commit/c1ee33f0cd65a23ec0090852ee0fc5fa50e72b64)) +- add rclone binary to docker image and arm64 support ([#105](https://github.com/garethgeorge/backrest/issues/105)) ([5a49f2f](https://github.com/garethgeorge/backrest/commit/5a49f2f063e887cba85bba0347ebce3efe15753e)) +- bundle rclone, busybox commands, and bash in default backrest docker image ([cec04f8](https://github.com/garethgeorge/backrest/commit/cec04f8f745d4bcfd49829c43367c61cb9778174)) +- display non-fatal errors in backup operations (e.g. unreadable files) in UI ([#100](https://github.com/garethgeorge/backrest/issues/100)) ([caac35a](https://github.com/garethgeorge/backrest/commit/caac35a5402d056b626b59d19084d6a699d4346d)) + +### Bug Fixes + +- improve error message when rclone config is missing ([663b430](https://github.com/garethgeorge/backrest/commit/663b430598e0890df74989af12ae81fae7922251)) +- improved sidebar status refresh interval during live operations ([3d192fd](https://github.com/garethgeorge/backrest/commit/3d192fd59d98c242ed583d00eeec37e68a4a2ff5)) +- live backup progress updates with partial-backup errors ([97a4948](https://github.com/garethgeorge/backrest/commit/97a494847ac5031866c31db0bb32219e6b2a0038)) +- migrate prune policy options to oneof ([ef41d34](https://github.com/garethgeorge/backrest/commit/ef41d34d5312b6a3bcc4af536f64275cd20da657)) +- restore operations should succeed for unassociated snapshots ([448107d](https://github.com/garethgeorge/backrest/commit/448107d22612f040fd45493246088277a4a72f63)) +- separate docker images for scratch and alpine linux base ([#106](https://github.com/garethgeorge/backrest/issues/106)) ([40e3e04](https://github.com/garethgeorge/backrest/commit/40e3e04a686f0a1749fa39e15821e6310e0ccf52)) + +## [0.12.2](https://github.com/garethgeorge/backrest/compare/v0.12.1...v0.12.2) (2024-02-16) + +### Bug Fixes + +- release-please automation ([63ddf15](https://github.com/garethgeorge/backrest/commit/63ddf15bf9799de30bda8548421e11e1bcd9ed05)) + +## [0.12.1](https://github.com/garethgeorge/backrest/compare/v0.12.0...v0.12.1) (2024-02-16) + +### Bug Fixes + +- delete event button in UI is hard to see on light theme ([8a05df8](https://github.com/garethgeorge/backrest/commit/8a05df87fcc44699c890f0cbe1065d79f49e1cc2)) +- use 'embed' to package WebUI sources instead of go.rice ([e3ba5cf](https://github.com/garethgeorge/backrest/commit/e3ba5cf12ebfedafaa2125687bd7522f29ccab51)) + +## [0.12.0](https://github.com/garethgeorge/backrest/compare/v0.11.1...v0.12.0) (2024-02-15) + +### Features + +- add button to forget individual snapshots ([276b1d2](https://github.com/garethgeorge/backrest/commit/276b1d2c602ad0f787958452070771af3e69f073)) +- add slack webhook ([8fa90ab](https://github.com/garethgeorge/backrest/commit/8fa90ab9ca48f0888ed0a5d263cb697758063188)) +- Add support for multiple sets of expected env vars per repo scheme ([#90](https://github.com/garethgeorge/backrest/issues/90)) ([da0551c](https://github.com/garethgeorge/backrest/commit/da0551c19a98fe675d278e34f8e3cc58ac9edaf5)) +- clear operations from history ([dc7a3a5](https://github.com/garethgeorge/backrest/commit/dc7a3a59a2400f97dd6b8140c6e70a34105496f9)) +- Windows WebUI uses correct path separator ([f5521e7](https://github.com/garethgeorge/backrest/commit/f5521e7b56e446fa2062a95560f315621b77d3e6)) + +### Bug Fixes + +- cleanup old versions of restic when upgrading ([79f529f](https://github.com/garethgeorge/backrest/commit/79f529f8edfb9bf893e74f7b1355bd7f2d7bdc3f)) +- hide delete operation button if operation is in progress or pending ([08c8762](https://github.com/garethgeorge/backrest/commit/08c876243febb99a68740c449055e850f37d740e)) +- retention policy configuration in add plan view ([dd24d90](https://github.com/garethgeorge/backrest/commit/dd24d9024f5ade62535956b1449dae75627ce493)) +- stats operations running at wrong interval ([05e5ae0](https://github.com/garethgeorge/backrest/commit/05e5ae0c455680bf9fbc9b4b2a9fbf96bcfdfc3b)) + +## [0.11.1](https://github.com/garethgeorge/backrest/compare/v0.11.0...v0.11.1) (2024-02-08) + +### Bug Fixes + +- backrest fails to create directory for jwt secrets ([0067edf](https://github.com/garethgeorge/backrest/commit/0067edf378b01147f0041c225994098cb9c452ab)) +- form bugs in UI e.g. awkward behavior when modifying hooks ([4fcf526](https://github.com/garethgeorge/backrest/commit/4fcf52602c114e2c639fc4302a9b8e8d51180a4d)) +- update restic version to 1.16.4 ([668a7cb](https://github.com/garethgeorge/backrest/commit/668a7cb5bb5c0955a0e3186b2dd9329cedddd96f)) +- wrong field names in hooks form ([3540904](https://github.com/garethgeorge/backrest/commit/354090497b73d40d8a9e705d1aa0c4662ffc4b0e)) +- wrong value passed to --max-unused when providing a custom prune policy ([34175f2](https://github.com/garethgeorge/backrest/commit/34175f273630f7d2324a4d6b5f9f2f7576dd6608)) + +## [0.11.0](https://github.com/garethgeorge/backrest/compare/v0.10.1...v0.11.0) (2024-02-04) + +### Features + +- add user configurable command hooks for backup lifecycle events ([#60](https://github.com/garethgeorge/backrest/issues/60)) ([9be413b](https://github.com/garethgeorge/backrest/commit/9be413bbcca796862f161a769991ab695a50b464)) +- authentication for WebUI ([#62](https://github.com/garethgeorge/backrest/issues/62)) ([4a1f326](https://github.com/garethgeorge/backrest/commit/4a1f3268a7de0533e0a979b9e97a7117b028358e)) +- implement discord hook type ([25924b6](https://github.com/garethgeorge/backrest/commit/25924b6197c870f9dfc1e04f5be39377251e7f2d)) +- implement gotify hook type ([e0ce655](https://github.com/garethgeorge/backrest/commit/e0ce6558c047f3aff068ee5d475fa1bdba380c4d)) +- support keep-all retention policy for append-only backups ([f163c02](https://github.com/garethgeorge/backrest/commit/f163c02d7d2c798b4057037a996de44e34de9f2b)) + +### Bug Fixes + +- add API test coverage and fix minor bugs ([f5bb74b](https://github.com/garethgeorge/backrest/commit/f5bb74bf246fcd5712dbbc85f4233169f7db4aa7)) +- add first time setup hint for user authentication ([4a565f2](https://github.com/garethgeorge/backrest/commit/4a565f2cdcd091e0eabc302ab91e53012f53eb26)) +- add test coverage for log rotation ([f1084ca](https://github.com/garethgeorge/backrest/commit/f1084cab4894751ba4a92f9be6b6b70d9084d0e6)) +- bugfixes for auth flow ([427792c](https://github.com/garethgeorge/backrest/commit/427792c7244fb712bbea0557d4a6c7ee07052534)) +- stats not displaying on long running repos ([f1ba1d9](https://github.com/garethgeorge/backrest/commit/f1ba1d91f37234f24ae5202d27114a33432366da)) +- store large log outputs in tar bundles of logs ([0cf01e0](https://github.com/garethgeorge/backrest/commit/0cf01e020640b0145bcd0d25a38cde1fce940aff)) +- windows install errors on decompressing zip archive ([5323b9f](https://github.com/garethgeorge/backrest/commit/5323b9ffc065bc3b28171575cdccc4358b69750b)) + +## [0.10.1](https://github.com/garethgeorge/backrest/compare/v0.10.0...v0.10.1) (2024-01-25) + +### Bug Fixes + +- chmod config 0600 such that only the creating user can read ([ecff0e5](https://github.com/garethgeorge/backrest/commit/ecff0e57c1fa4d65f35774d227a27222af8e7921)) +- install scripts handle working dir correctly ([dcff2ad](https://github.com/garethgeorge/backrest/commit/dcff2adf60222030043d7a227d27e74f555ab376)) +- relax name regex for plans and repos ([ee6134a](https://github.com/garethgeorge/backrest/commit/ee6134af76c3e90f542f67b89b2571f060db5590)) +- sftp support using public key authentication ([bedb302](https://github.com/garethgeorge/backrest/commit/bedb302a025438a58309f26b046c9b6d49316414)) +- typos in validation error messages in addrepomodel ([3b79afb](https://github.com/garethgeorge/backrest/commit/3b79afb2b18530deaa10cca08a60941a64c6fd9b)) + +## [0.10.0](https://github.com/garethgeorge/backrest/compare/v0.9.3...v0.10.0) (2024-01-15) + +### Features + +- make prune policy configurable in the addrepoview in the UI ([3fd08eb](https://github.com/garethgeorge/backrest/commit/3fd08eb8e4b455db656a0680318851824fdad2db)) +- update restic dependency to v0.16.3 ([ac8db31](https://github.com/garethgeorge/backrest/commit/ac8db31713d4db3c2240b7f7c006e518e9e0726c)) +- verify gpg signature when downloading and installing restic binary ([04106d1](https://github.com/garethgeorge/backrest/commit/04106d15d5ad73db6e670e84340ac1f9be200a23)) + +## [0.9.3](https://github.com/garethgeorge/backrest/compare/v0.9.2...v0.9.3) (2024-01-05) + +### Bug Fixes + +- correctly mark tasks as inprogress before execution ([b19438a](https://github.com/garethgeorge/backrest/commit/b19438afbd7b83dc964774347e64491143a3a5d2)) +- correctly select light/dark mode based on system colortheme ([b64199c](https://github.com/garethgeorge/backrest/commit/b64199c140db7d2a77b58219cee088d22ec81954)) + +## [0.9.2](https://github.com/garethgeorge/backrest/compare/v0.9.1...v0.9.2) (2024-01-01) + +### Bug Fixes + +- possible race condition in scheduled task heap ([30874c9](https://github.com/garethgeorge/backrest/commit/30874c9150f32a0fba5f1ea99bc77bcc978d8b03)) + +## [0.9.1](https://github.com/garethgeorge/backrest/compare/v0.9.0...v0.9.1) (2024-01-01) + +### Bug Fixes + +- failed forget operations are hidden in the UI ([9896446](https://github.com/garethgeorge/backrest/commit/9896446ccfbcb8475a21b5fb565ebb73cb6bac2c)) +- UI buttons spin while waiting for tasks to complete ([c767fa7](https://github.com/garethgeorge/backrest/commit/c767fa7476d76f1b4eb49443a19ee1cedb4eb70a)) + +## [0.9.0](https://github.com/garethgeorge/backrest/compare/v0.8.1...v0.9.0) (2023-12-31) + +### Features + +- add backrest logo ([5add0d8](https://github.com/garethgeorge/backrest/commit/5add0d8ffa829a71103520c94eacae17966f2a9f)) +- add mobile layout ([9c7f227](https://github.com/garethgeorge/backrest/commit/9c7f227ad0f5df34d66390c94b64e9f5181d24f0)) +- index snapshots created outside of backrest ([7711297](https://github.com/garethgeorge/backrest/commit/7711297a84170a733c5ccdb3e89617efc878cf69)) +- schedule index operations and stats refresh from repo view ([851bd12](https://github.com/garethgeorge/backrest/commit/851bd125b640e65a5b98b67d28d2f29e94411646)) + +### Bug Fixes + +- operations associated with incorrect ID when tasks are rescheduled ([25871c9](https://github.com/garethgeorge/backrest/commit/25871c99920d8717e91bf1a921109b9df82a59a1)) +- reduce stats refresh frequency ([adbe005](https://github.com/garethgeorge/backrest/commit/adbe0056d82a5d9f890ce79b1120f5084bdc7124)) +- stat never runs ([3f3252d](https://github.com/garethgeorge/backrest/commit/3f3252d47951270fbf5f21b0831effb121d3ba3f)) +- stats task priority ([6bfe769](https://github.com/garethgeorge/backrest/commit/6bfe769fe037a5f2d35947574a5ed7e26ba981a8)) +- tasks run late when laptops resume from sleep ([cb78298](https://github.com/garethgeorge/backrest/commit/cb78298cffb492560717d5f8bdcd5941f7976f2e)) +- UI and code quality improvements ([c5e435d](https://github.com/garethgeorge/backrest/commit/c5e435d640bc8e79ceacf7f64d4cf75644859204)) + +## [0.8.0](https://github.com/garethgeorge/backrest/compare/v0.7.0...v0.8.0) (2023-12-25) + +### Features + +- add repo stats to restic package ([26d4724](https://github.com/garethgeorge/backrest/commit/26d47243c1e31f17c4d8adc6227325551854ce1f)) +- add stats to repo view e.g. total size in storage ([adb0e3f](https://github.com/garethgeorge/backrest/commit/adb0e3f23050a86cd1c507d374e9d45f5eb5ee27)) +- display last operation status for each plan and repo in UI ([cc11197](https://github.com/garethgeorge/backrest/commit/cc111970ca2e61cf39804378808aa5b5f77f9581)) + +### Bug Fixes + +- crashing bug on partial backup ([#39](https://github.com/garethgeorge/backrest/issues/39)) ([fba6c8d](https://github.com/garethgeorge/backrest/commit/fba6c8da869d66b7b44f87a0dc1e3779924c31b7)) +- install scripts and improved asset compression ([b8c2e81](https://github.com/garethgeorge/backrest/commit/b8c2e813586f2b48c78d70e09a29c5052621caf1)) + +## [0.7.0](https://github.com/garethgeorge/backrest/compare/v0.6.0...v0.7.0) (2023-12-22) + +### Features + +- add activity bar to UI heading ([f5c3e76](https://github.com/garethgeorge/backrest/commit/f5c3e762ed4ed3c908e843d74985fb6c7b253db7)) +- add clear error history button ([48d80b9](https://github.com/garethgeorge/backrest/commit/48d80b9473db6619518924d0849b0eda78e62afa)) +- add repo view ([9522ac1](https://github.com/garethgeorge/backrest/commit/9522ac18deedc15311d3d464ee36c20e7f72e39f)) +- autoinstall required restic version ([b385c01](https://github.com/garethgeorge/backrest/commit/b385c011210087e6d6992a4e4b279fec4b22ab89)) +- basic forget support in backend and UI ([d22d9d1](https://github.com/garethgeorge/backrest/commit/d22d9d1a05831fae94ce397c0c73c6292d378cf5)) +- begin UI integration with backend ([cccdd29](https://github.com/garethgeorge/backrest/commit/cccdd297c15cd47268b2a1903e9624bdbca3dc68)) +- display queued operations ([0c818bb](https://github.com/garethgeorge/backrest/commit/0c818bb9452a944d8b1127e553142e1e60ed90af)) +- forget soft-deletes operations associated with removed snapshots ([f3dc7ff](https://github.com/garethgeorge/backrest/commit/f3dc7ffd077fef67870852f8f4e8b9aa6c94806e)) +- forget soft-deletes operations associated with removed snapshots ([38bc107](https://github.com/garethgeorge/backrest/commit/38bc107db394716e34245f1edefc5e4cf4a15333)) +- implement add plan UI ([9288589](https://github.com/garethgeorge/backrest/commit/92885898cf551a2dcb4bb315f130138cd7a8cc67)) +- implement backup scheduling in orchestrator ([eadb1a8](https://github.com/garethgeorge/backrest/commit/eadb1a82019f0cfc82edf8559adbad7730a4e86a)) +- implement basic plan view ([4c6f042](https://github.com/garethgeorge/backrest/commit/4c6f042250946a036e46225e669ee39e2433b198)) +- implement delete button for plan and repo UIs ([ffb0d85](https://github.com/garethgeorge/backrest/commit/ffb0d859f19f4af66a7521768dab083995f9672a)) +- implement forget and prune support in restic pkg ([ffb4573](https://github.com/garethgeorge/backrest/commit/ffb4573737a73cc32f325bc0b9c3feed764b7879)) +- implement forget operation ([ebccf3b](https://github.com/garethgeorge/backrest/commit/ebccf3bc3b78083aee635de7c6ae23b52ee88284)) +- implement garbage collection of old operations ([46456a8](https://github.com/garethgeorge/backrest/commit/46456a88870934506ede4b67c3dfaa2f2afcee14)) +- implement prune support ([#25](https://github.com/garethgeorge/backrest/issues/25)) ([a311b0a](https://github.com/garethgeorge/backrest/commit/a311b0a3fb5315f17d66361a3e72fa10b8a744a1)) +- implement repo unlocking and operation list implementation ([6665ad9](https://github.com/garethgeorge/backrest/commit/6665ad98d7f54bea30ea532932a8a3409717c913)) +- implement repo, edit, and supporting RPCs ([d282c32](https://github.com/garethgeorge/backrest/commit/d282c32c8bd3d8f5747e934d4af6a84faca1ec86)) +- implement restore operation through snapshot browser UI ([#27](https://github.com/garethgeorge/backrest/issues/27)) ([d758509](https://github.com/garethgeorge/backrest/commit/d758509797e21e3ec4bc67eff4d974604e4a5476)) +- implement snapshot browsing ([8ffffa0](https://github.com/garethgeorge/backrest/commit/8ffffa05e41ca31e2d38fde5427dae34ac4a1abb)) +- implement snapshot indexing ([a90b30e](https://github.com/garethgeorge/backrest/commit/a90b30e19f7107874bbfe244451b07f72c437213)) +- improve oplist performance and display forget operations in oplist ([#22](https://github.com/garethgeorge/backrest/issues/22)) ([51b4921](https://github.com/garethgeorge/backrest/commit/51b49214e3d32cc4b28e13085bd196ba164a8c19)) +- initial oplog implementation ([dd9142c](https://github.com/garethgeorge/backrest/commit/dd9142c0e97e1175ff12f2861220af0e0d68b7d9)) +- initial optree implementation ([ba390a2](https://github.com/garethgeorge/backrest/commit/ba390a2ca1b5e9adaab36a7db0d988f54f5a6cdd)) +- initial Windows OS support ([f048cbf](https://github.com/garethgeorge/backrest/commit/f048cbf10dc60da51cd7f5aee4614a8750fd85b2)) +- match system color theme (darkmode support) ([a8762dc](https://github.com/garethgeorge/backrest/commit/a8762dca329927b93db40b01cc011c00e12891f0)) +- operations IDs are ordered by operation timestamp ([a1ed6f9](https://github.com/garethgeorge/backrest/commit/a1ed6f90ba1d608e00c53221db45b67251085aa7)) +- present list of operations on plan view ([6491dbe](https://github.com/garethgeorge/backrest/commit/6491dbed146967c0e12eee4392d1d12843dc7c5e)) +- repo can be created through UI ([9ccade5](https://github.com/garethgeorge/backrest/commit/9ccade5ccd97f4e485d52ad5c675be6b0a4a1049)) +- scaffolding basic UI structure ([1273f81](https://github.com/garethgeorge/backrest/commit/1273f8105a2549b0ccd0c7a588eb60646b66366e)) +- show snapshots in sidenav ([1a9a5b6](https://github.com/garethgeorge/backrest/commit/1a9a5b60d24dd75752e5a3f84dd87af3e38422bb)) +- snapshot items are viewable in the UI and minor element ordering fixes ([a333001](https://github.com/garethgeorge/backrest/commit/a33300175c645f31b95b3038de02821a1f3d5559)) +- support ImportSnapshotOperation in oplog ([89f95b3](https://github.com/garethgeorge/backrest/commit/89f95b351fe250534cd39ac27ff34b2b148256e1)) +- support task cancellation ([fc9c06d](https://github.com/garethgeorge/backrest/commit/fc9c06df00409b73dda23f4be031746f492b1a24)) +- update getting started guide ([2c421d6](https://github.com/garethgeorge/backrest/commit/2c421d661501fa4a3120aa3f39937cd58b29c2dc)) + +### Bug Fixes + +- backup ordering in tree view ([b9c8b3e](https://github.com/garethgeorge/backrest/commit/b9c8b3e378e88a0feff4d477d9d97eb5db075382)) +- build and test fixes ([4957496](https://github.com/garethgeorge/backrest/commit/49574967871494dcb5095e5699610097466f57f9)) +- connectivity issues with embedded server ([482cc8e](https://github.com/garethgeorge/backrest/commit/482cc8ebbc93b919991f6566b212247c5874f70f)) +- deadlock in snapshots ([93b2120](https://github.com/garethgeorge/backrest/commit/93b2120f74ea348e5084ab430573368bf4066eec)) +- forget deadlocking and misc smaller bugs ([b7c633d](https://github.com/garethgeorge/backrest/commit/b7c633d021d68d4880a5f442ce70a858002b4af2)) +- hide no-op prune operations ([20dd78c](https://github.com/garethgeorge/backrest/commit/20dd78cac4bdd6385cb7a0ea9ff0be75fde9135b)) +- improve error message formatting ([ae33b01](https://github.com/garethgeorge/backrest/commit/ae33b01de408af3b1d711a369298a2782a24ad1e)) +- improve operation ordering to fix snapshots indexed before forget operation ([#21](https://github.com/garethgeorge/backrest/issues/21)) ([b513b08](https://github.com/garethgeorge/backrest/commit/b513b08e51434c28c90f5f062b4ae292f6854f4e)) +- improve output detail collection for command failures ([c492f9b](https://github.com/garethgeorge/backrest/commit/c492f9ba63169942509349797ebe951879b53635)) +- improve UI performance ([8488d46](https://github.com/garethgeorge/backrest/commit/8488d461bd7ffec2e8171d67f83093c32c79073f)) +- improve Windows path handling ([426aad4](https://github.com/garethgeorge/backrest/commit/426aad4890d2de5d70cd2e0232c0d11c42606c92)) +- incorrrect handling of oplog events in UI ([95ca96a](https://github.com/garethgeorge/backrest/commit/95ca96a31f2e1ead2702164ec8675e4b4f54cf1d)) +- operation ordering in tree view ([2b4e1a2](https://github.com/garethgeorge/backrest/commit/2b4e1a2fdbf11b010ddbcd0b6fd2640d01e4dbc8)) +- operations marked as 'warn' rather than 'error' for partial backups ([fe92b62](https://github.com/garethgeorge/backrest/commit/fe92b625780481193e0ab63fbbdddb889bbda2a8)) +- ordering of operations when viewed from backup tree ([063f086](https://github.com/garethgeorge/backrest/commit/063f086a6e31df250dd9be42cdb5fa549307106f)) +- race condition in snapshot browser ([f239b91](https://github.com/garethgeorge/backrest/commit/f239b9170415e063ec8d60a5b5e14ae3610b9bad)) +- relax output parsing to skip over warnings ([8f85b74](https://github.com/garethgeorge/backrest/commit/8f85b747f57844bbc898668723eec50a1666aa39)) +- repo orchestrator tests ([d077fc8](https://github.com/garethgeorge/backrest/commit/d077fc83c97b7fbdbeda9702828c8780182b2616)) +- restic fails to detect summary event for very short backups ([46b2a85](https://github.com/garethgeorge/backrest/commit/46b2a8567706ddb21cfcf3e18b57e16d50809b56)) +- restic should initialize repo on backup operation ([e57abbd](https://github.com/garethgeorge/backrest/commit/e57abbdcb1864c362e6ae3c22850c0380671cb34)) +- restora should not init repos added manually e.g. without the UI ([68b50e1](https://github.com/garethgeorge/backrest/commit/68b50e1eb5a2ebd861c869f71f49d196cb5214f8)) +- snapshots out of order in UI ([b9bcc7e](https://github.com/garethgeorge/backrest/commit/b9bcc7e7c758abafa4878b6ef895adf2d2d0bc42)) +- standardize on fully qualified snapshot_id and decouple protobufs from restic package ([e6031bf](https://github.com/garethgeorge/backrest/commit/e6031bfa543a7300e622c1b0f56efc6320e7611e)) +- support more versions of restic ([0cdfd11](https://github.com/garethgeorge/backrest/commit/0cdfd115e29a0b08d5814e71c0f4a8f2baf52e90)) +- task cancellation ([d49b729](https://github.com/garethgeorge/backrest/commit/d49b72996ea7fd0543d55db3fc8e1127fe5a2476)) +- task priority not taking effect ([af7462c](https://github.com/garethgeorge/backrest/commit/af7462cefb130153cdaaa08e8ebefefa40e80e49)) +- time formatting in operation list ([53c7f12](https://github.com/garethgeorge/backrest/commit/53c7f1248f5284080fff872ac79b3996474412b3)) +- UI layout adjustments ([7d1b95c](https://github.com/garethgeorge/backrest/commit/7d1b95c81f0f69840ce1d20cb0d4a4bb90011dc9)) +- unexpected config location on MacOS ([8d40576](https://github.com/garethgeorge/backrest/commit/8d40576c6526d6f180c96fbeb81d7f59f56b51b8)) +- use timezone offset when grouping operations in OperationTree ([06240bd](https://github.com/garethgeorge/backrest/commit/06240bd7adabd76424025030cfde2fb5e54c219f)) + +## [0.6.0](https://github.com/garethgeorge/backrest/compare/v0.5.0...v0.6.0) (2023-12-22) + +### Features + +- add activity bar to UI heading ([f5c3e76](https://github.com/garethgeorge/backrest/commit/f5c3e762ed4ed3c908e843d74985fb6c7b253db7)) +- add clear error history button ([48d80b9](https://github.com/garethgeorge/backrest/commit/48d80b9473db6619518924d0849b0eda78e62afa)) +- add repo view ([9522ac1](https://github.com/garethgeorge/backrest/commit/9522ac18deedc15311d3d464ee36c20e7f72e39f)) +- implement garbage collection of old operations ([46456a8](https://github.com/garethgeorge/backrest/commit/46456a88870934506ede4b67c3dfaa2f2afcee14)) +- support task cancellation ([fc9c06d](https://github.com/garethgeorge/backrest/commit/fc9c06df00409b73dda23f4be031746f492b1a24)) + +### Bug Fixes + +- backup ordering in tree view ([b9c8b3e](https://github.com/garethgeorge/backrest/commit/b9c8b3e378e88a0feff4d477d9d97eb5db075382)) +- hide no-op prune operations ([20dd78c](https://github.com/garethgeorge/backrest/commit/20dd78cac4bdd6385cb7a0ea9ff0be75fde9135b)) +- incorrrect handling of oplog events in UI ([95ca96a](https://github.com/garethgeorge/backrest/commit/95ca96a31f2e1ead2702164ec8675e4b4f54cf1d)) +- operation ordering in tree view ([2b4e1a2](https://github.com/garethgeorge/backrest/commit/2b4e1a2fdbf11b010ddbcd0b6fd2640d01e4dbc8)) +- operations marked as 'warn' rather than 'error' for partial backups ([fe92b62](https://github.com/garethgeorge/backrest/commit/fe92b625780481193e0ab63fbbdddb889bbda2a8)) +- race condition in snapshot browser ([f239b91](https://github.com/garethgeorge/backrest/commit/f239b9170415e063ec8d60a5b5e14ae3610b9bad)) +- restic should initialize repo on backup operation ([e57abbd](https://github.com/garethgeorge/backrest/commit/e57abbdcb1864c362e6ae3c22850c0380671cb34)) +- backrest should not init repos added manually e.g. without the UI ([68b50e1](https://github.com/garethgeorge/backrest/commit/68b50e1eb5a2ebd861c869f71f49d196cb5214f8)) +- task cancellation ([d49b729](https://github.com/garethgeorge/backrest/commit/d49b72996ea7fd0543d55db3fc8e1127fe5a2476)) +- use timezone offset when grouping operations in OperationTree ([06240bd](https://github.com/garethgeorge/backrest/commit/06240bd7adabd76424025030cfde2fb5e54c219f)) + +## [0.5.0](https://github.com/garethgeorge/backrest/compare/v0.4.0...v0.5.0) (2023-12-10) + +### Features + +- implement repo unlocking and operation list implementation ([6665ad9](https://github.com/garethgeorge/backrest/commit/6665ad98d7f54bea30ea532932a8a3409717c913)) +- initial Windows OS support ([f048cbf](https://github.com/garethgeorge/backrest/commit/f048cbf10dc60da51cd7f5aee4614a8750fd85b2)) +- match system color theme (darkmode support) ([a8762dc](https://github.com/garethgeorge/backrest/commit/a8762dca329927b93db40b01cc011c00e12891f0)) + +### Bug Fixes + +- improve output detail collection for command failures ([c492f9b](https://github.com/garethgeorge/backrest/commit/c492f9ba63169942509349797ebe951879b53635)) +- improve Windows path handling ([426aad4](https://github.com/garethgeorge/backrest/commit/426aad4890d2de5d70cd2e0232c0d11c42606c92)) +- ordering of operations when viewed from backup tree ([063f086](https://github.com/garethgeorge/backrest/commit/063f086a6e31df250dd9be42cdb5fa549307106f)) +- relax output parsing to skip over warnings ([8f85b74](https://github.com/garethgeorge/backrest/commit/8f85b747f57844bbc898668723eec50a1666aa39)) +- snapshots out of order in UI ([b9bcc7e](https://github.com/garethgeorge/backrest/commit/b9bcc7e7c758abafa4878b6ef895adf2d2d0bc42)) +- unexpected config location on MacOS ([8d40576](https://github.com/garethgeorge/backrest/commit/8d40576c6526d6f180c96fbeb81d7f59f56b51b8)) + +## [0.4.0](https://github.com/garethgeorge/backrest/compare/v0.3.0...v0.4.0) (2023-12-04) + +### Features + +- implement prune support ([#25](https://github.com/garethgeorge/backrest/issues/25)) ([a311b0a](https://github.com/garethgeorge/backrest/commit/a311b0a3fb5315f17d66361a3e72fa10b8a744a1)) +- implement restore operation through snapshot browser UI ([#27](https://github.com/garethgeorge/backrest/issues/27)) ([d758509](https://github.com/garethgeorge/backrest/commit/d758509797e21e3ec4bc67eff4d974604e4a5476)) + +## [0.3.0](https://github.com/garethgeorge/backrest/compare/v0.2.0...v0.3.0) (2023-12-03) + +### Features + +- autoinstall required restic version ([b385c01](https://github.com/garethgeorge/backrest/commit/b385c011210087e6d6992a4e4b279fec4b22ab89)) +- basic forget support in backend and UI ([d22d9d1](https://github.com/garethgeorge/backrest/commit/d22d9d1a05831fae94ce397c0c73c6292d378cf5)) +- begin UI integration with backend ([cccdd29](https://github.com/garethgeorge/backrest/commit/cccdd297c15cd47268b2a1903e9624bdbca3dc68)) +- display queued operations ([0c818bb](https://github.com/garethgeorge/backrest/commit/0c818bb9452a944d8b1127e553142e1e60ed90af)) +- forget soft-deletes operations associated with removed snapshots ([f3dc7ff](https://github.com/garethgeorge/backrest/commit/f3dc7ffd077fef67870852f8f4e8b9aa6c94806e)) +- forget soft-deletes operations associated with removed snapshots ([38bc107](https://github.com/garethgeorge/backrest/commit/38bc107db394716e34245f1edefc5e4cf4a15333)) +- implement add plan UI ([9288589](https://github.com/garethgeorge/backrest/commit/92885898cf551a2dcb4bb315f130138cd7a8cc67)) +- implement backup scheduling in orchestrator ([eadb1a8](https://github.com/garethgeorge/backrest/commit/eadb1a82019f0cfc82edf8559adbad7730a4e86a)) +- implement basic plan view ([4c6f042](https://github.com/garethgeorge/backrest/commit/4c6f042250946a036e46225e669ee39e2433b198)) +- implement delete button for plan and repo UIs ([ffb0d85](https://github.com/garethgeorge/backrest/commit/ffb0d859f19f4af66a7521768dab083995f9672a)) +- implement forget and prune support in restic pkg ([ffb4573](https://github.com/garethgeorge/backrest/commit/ffb4573737a73cc32f325bc0b9c3feed764b7879)) +- implement forget operation ([ebccf3b](https://github.com/garethgeorge/backrest/commit/ebccf3bc3b78083aee635de7c6ae23b52ee88284)) +- implement repo, edit, and supporting RPCs ([d282c32](https://github.com/garethgeorge/backrest/commit/d282c32c8bd3d8f5747e934d4af6a84faca1ec86)) +- implement snapshot browsing ([8ffffa0](https://github.com/garethgeorge/backrest/commit/8ffffa05e41ca31e2d38fde5427dae34ac4a1abb)) +- implement snapshot indexing ([a90b30e](https://github.com/garethgeorge/backrest/commit/a90b30e19f7107874bbfe244451b07f72c437213)) +- improve oplist performance and display forget operations in oplist ([#22](https://github.com/garethgeorge/backrest/issues/22)) ([51b4921](https://github.com/garethgeorge/backrest/commit/51b49214e3d32cc4b28e13085bd196ba164a8c19)) +- initial oplog implementation ([dd9142c](https://github.com/garethgeorge/backrest/commit/dd9142c0e97e1175ff12f2861220af0e0d68b7d9)) +- initial optree implementation ([ba390a2](https://github.com/garethgeorge/backrest/commit/ba390a2ca1b5e9adaab36a7db0d988f54f5a6cdd)) +- operations IDs are ordered by operation timestamp ([a1ed6f9](https://github.com/garethgeorge/backrest/commit/a1ed6f90ba1d608e00c53221db45b67251085aa7)) +- present list of operations on plan view ([6491dbe](https://github.com/garethgeorge/backrest/commit/6491dbed146967c0e12eee4392d1d12843dc7c5e)) +- repo can be created through UI ([9ccade5](https://github.com/garethgeorge/backrest/commit/9ccade5ccd97f4e485d52ad5c675be6b0a4a1049)) +- scaffolding basic UI structure ([1273f81](https://github.com/garethgeorge/backrest/commit/1273f8105a2549b0ccd0c7a588eb60646b66366e)) +- show snapshots in sidenav ([1a9a5b6](https://github.com/garethgeorge/backrest/commit/1a9a5b60d24dd75752e5a3f84dd87af3e38422bb)) +- snapshot items are viewable in the UI and minor element ordering fixes ([a333001](https://github.com/garethgeorge/backrest/commit/a33300175c645f31b95b3038de02821a1f3d5559)) +- support ImportSnapshotOperation in oplog ([89f95b3](https://github.com/garethgeorge/backrest/commit/89f95b351fe250534cd39ac27ff34b2b148256e1)) +- update getting started guide ([2c421d6](https://github.com/garethgeorge/backrest/commit/2c421d661501fa4a3120aa3f39937cd58b29c2dc)) + +### Bug Fixes + +- build and test fixes ([4957496](https://github.com/garethgeorge/backrest/commit/49574967871494dcb5095e5699610097466f57f9)) +- connectivity issues with embedded server ([482cc8e](https://github.com/garethgeorge/backrest/commit/482cc8ebbc93b919991f6566b212247c5874f70f)) +- deadlock in snapshots ([93b2120](https://github.com/garethgeorge/backrest/commit/93b2120f74ea348e5084ab430573368bf4066eec)) +- forget deadlocking and misc smaller bugs ([b7c633d](https://github.com/garethgeorge/backrest/commit/b7c633d021d68d4880a5f442ce70a858002b4af2)) +- improve error message formatting ([ae33b01](https://github.com/garethgeorge/backrest/commit/ae33b01de408af3b1d711a369298a2782a24ad1e)) +- improve operation ordering to fix snapshots indexed before forget operation ([#21](https://github.com/garethgeorge/backrest/issues/21)) ([b513b08](https://github.com/garethgeorge/backrest/commit/b513b08e51434c28c90f5f062b4ae292f6854f4e)) +- improve UI performance ([8488d46](https://github.com/garethgeorge/backrest/commit/8488d461bd7ffec2e8171d67f83093c32c79073f)) +- repo orchestrator tests ([d077fc8](https://github.com/garethgeorge/backrest/commit/d077fc83c97b7fbdbeda9702828c8780182b2616)) +- restic fails to detect summary event for very short backups ([46b2a85](https://github.com/garethgeorge/backrest/commit/46b2a8567706ddb21cfcf3e18b57e16d50809b56)) +- standardize on fully qualified snapshot_id and decouple protobufs from restic package ([e6031bf](https://github.com/garethgeorge/backrest/commit/e6031bfa543a7300e622c1b0f56efc6320e7611e)) +- support more versions of restic ([0cdfd11](https://github.com/garethgeorge/backrest/commit/0cdfd115e29a0b08d5814e71c0f4a8f2baf52e90)) +- task priority not taking effect ([af7462c](https://github.com/garethgeorge/backrest/commit/af7462cefb130153cdaaa08e8ebefefa40e80e49)) +- time formatting in operation list ([53c7f12](https://github.com/garethgeorge/backrest/commit/53c7f1248f5284080fff872ac79b3996474412b3)) +- UI layout adjustments ([7d1b95c](https://github.com/garethgeorge/backrest/commit/7d1b95c81f0f69840ce1d20cb0d4a4bb90011dc9)) + +## [0.2.0](https://github.com/garethgeorge/backrest/compare/v0.1.3...v0.2.0) (2023-12-03) + +### Features + +- forget soft-deletes operations associated with removed snapshots ([f3dc7ff](https://github.com/garethgeorge/backrest/commit/f3dc7ffd077fef67870852f8f4e8b9aa6c94806e)) +- forget soft-deletes operations associated with removed snapshots ([38bc107](https://github.com/garethgeorge/backrest/commit/38bc107db394716e34245f1edefc5e4cf4a15333)) +- improve oplist performance and display forget operations in oplist ([#22](https://github.com/garethgeorge/backrest/issues/22)) ([51b4921](https://github.com/garethgeorge/backrest/commit/51b49214e3d32cc4b28e13085bd196ba164a8c19)) + +### Bug Fixes + +- forget deadlocking and misc smaller bugs ([b7c633d](https://github.com/garethgeorge/backrest/commit/b7c633d021d68d4880a5f442ce70a858002b4af2)) +- improve operation ordering to fix snapshots indexed before forget operation ([#21](https://github.com/garethgeorge/backrest/issues/21)) ([b513b08](https://github.com/garethgeorge/backrest/commit/b513b08e51434c28c90f5f062b4ae292f6854f4e)) +- task priority not taking effect ([af7462c](https://github.com/garethgeorge/backrest/commit/af7462cefb130153cdaaa08e8ebefefa40e80e49)) +- UI layout adjustments ([7d1b95c](https://github.com/garethgeorge/backrest/commit/7d1b95c81f0f69840ce1d20cb0d4a4bb90011dc9)) + +## [0.2.0](https://github.com/garethgeorge/backrest/compare/v0.1.3...v0.2.0) (2023-12-02) + +### Features + +- forget soft-deletes operations associated with removed snapshots ([f3dc7ff](https://github.com/garethgeorge/backrest/commit/f3dc7ffd077fef67870852f8f4e8b9aa6c94806e)) +- forget soft-deletes operations associated with removed snapshots ([38bc107](https://github.com/garethgeorge/backrest/commit/38bc107db394716e34245f1edefc5e4cf4a15333)) + +### Bug Fixes + +- forget deadlocking and misc smaller bugs ([b7c633d](https://github.com/garethgeorge/backrest/commit/b7c633d021d68d4880a5f442ce70a858002b4af2)) +- improve operation ordering to fix snapshots indexed before forget operation ([#21](https://github.com/garethgeorge/backrest/issues/21)) ([b513b08](https://github.com/garethgeorge/backrest/commit/b513b08e51434c28c90f5f062b4ae292f6854f4e)) +- task priority not taking effect ([af7462c](https://github.com/garethgeorge/backrest/commit/af7462cefb130153cdaaa08e8ebefefa40e80e49)) diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 000000000..307eef682 --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,10 @@ +FROM alpine:latest +LABEL org.opencontainers.image.source="https://github.com/garethgeorge/backrest" +RUN apk --no-cache add tini ca-certificates curl bash rclone openssh tzdata docker-cli +RUN mkdir -p /tmp +COPY backrest /backrest +RUN /backrest --install-deps-only +RUN mkdir -p /bin && mv /root/.local/share/backrest/restic /bin/restic + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/backrest", "--bind-address", ":9898"] diff --git a/Dockerfile.scratch b/Dockerfile.scratch new file mode 100644 index 000000000..cd11f9c1d --- /dev/null +++ b/Dockerfile.scratch @@ -0,0 +1,17 @@ +FROM alpine:latest AS alpine +RUN apk add --no-cache ca-certificates tini-static +RUN mkdir /tmp-orig +COPY backrest /backrest +RUN /backrest --install-deps-only +RUN mkdir -p /bin && mv /root/.local/share/backrest/restic /bin/restic + +FROM scratch +LABEL org.opencontainers.image.source="https://github.com/garethgeorge/backrest" +COPY --from=alpine /tmp-orig /tmp +COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=alpine /bin /bin +COPY --from=alpine /sbin/tini-static /tini +COPY backrest /backrest + +ENTRYPOINT ["/tini", "--"] +CMD ["/backrest", "--bind-address", ":9898"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index d090b1b83..85b57d2cd 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,279 @@ -# ResticUI +

-WIP project to build a UI for restic. +

+ + + +

-Project goals +--- - * Single binary for easy and _very lightweight_ deployment with or without containerization. - * WebUI supporting - * Backup plan creation and configuration - * Backup status - * Snapshot browsing and restore +**Overview** -# Dependencies +Backrest is a web-accessible backup solution built on top of [restic](https://restic.net/). Backrest provides a WebUI which wraps the restic CLI and makes it easy to create repos, browse snapshots, and restore files. Additionally, Backrest can run in the background and take an opinionated approach to scheduling snapshots and orchestrating repo health operations. -## Dev +By building on restic, Backrest leverages its mature, fast, reliable, and secure backup capabilities while adding an intuitive interface. + +Built with Go, Backrest is distributed as a standalone, lightweight binary with restic as its sole dependency. It can securely create new repositories or manage existing ones. Once storage is configured, the WebUI handles most operations, while still allowing direct access to the powerful [restic CLI](https://restic.readthedocs.io/en/latest/manual_rest.html) for advanced operations when needed. + +## Preview + +

+ + +

+ +## Key Features + +- **Web Interface**: Access locally or remotely (perfect for NAS deployments) +- **Multi-Platform Support**: + - Linux + - macOS + - Windows + - FreeBSD + - [Docker](https://hub.docker.com/r/garethgeorge/backrest) +- **Backup Management**: + - Import existing restic repositories + - Cron-scheduled backups and maintenance (e.g. prune, check, forget, etc) + - Browse and restore files from snapshots + - Configurable notifications (Discord, Slack, Shoutrrr, Gotify, Healthchecks) + - Pre/post backup command hooks to execute shell scripts +- **Storage Options**: + - Compatible with rclone remotes + - Supports all restic storage backends (S3, B2, Azure, GCS, local, SFTP, and [all rclone remotes](https://rclone.org/)) + +--- + +# User Guide + +[See the Backrest docs](https://garethgeorge.github.io/backrest/introduction/getting-started). + +# Installation + +Backrest is packaged as a single executable. It can be run directly on Linux, macOS, and Windows. [restic](https://github.com/restic/restic) will be downloaded and installed on first run. + +### Quick Start Options + +1. **Pre-built Release**: Download from the [releases page](https://github.com/garethgeorge/backrest/releases) +2. **Docker**: Use `garethgeorge/backrest:latest` ([Docker Hub](https://hub.docker.com/r/garethgeorge/backrest)) + - Includes rclone and common Unix utilities + - For minimal image, use `garethgeorge/backrest:scratch` +3. **Build from Source**: See [Building](#building) section below + +Once installed, access Backrest at `http://localhost:9898` (default port). First-time setup will prompt for username and password creation. + +> [!NOTE] +> To change the default port, set the `BACKREST_PORT` environment variable (e.g., `BACKREST_PORT=0.0.0.0:9898` to listen on all interfaces) +> +> Backrest will use your system's installed version of restic if it's available and compatible. If not, Backrest will download and install a suitable version in its data directory, keeping it updated. To use a specific restic binary, set the `BACKREST_RESTIC_COMMAND` environment variable to the desired path. + + +### Running with Docker Compose + +Docker image: https://hub.docker.com/r/garethgeorge/backrest + +Example compose file: + +```yaml +version: "3.8" +services: + backrest: + image: garethgeorge/backrest:latest + container_name: backrest + hostname: backrest + volumes: + - ./backrest/data:/data + - ./backrest/config:/config + - ./backrest/cache:/cache + - ./backrest/tmp:/tmp + - /path/to/backup/data:/userdata # Mount local paths to backup + - /path/to/local/repos:/repos # Mount local repos (optional for remote storage) + environment: + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TMPDIR=/tmp + - TZ=America/Los_Angeles + ports: + - "9898:9898" + restart: unless-stopped +``` + +## Running on Linux + +### Running on Linux + +1. **Download the Release** + - Get the latest release from the [releases page](https://github.com/garethgeorge/backrest/releases) + +2. **Installation Options** + + a) Using the Install Script (Recommended) + ```sh + mkdir backrest && tar -xzvf backrest_Linux_x86_64.tar.gz -C backrest + cd backrest && sudo ./install.sh + ``` + This script will: + - Move the Backrest binary to `/usr/local/bin` + - Create and start a systemd service + + b) Manual Installation with systemd + ```sh + sudo mv backrest /usr/local/bin/backrest + sudo tee /etc/systemd/system/backrest.service > /dev/null </dev/null; echo "@reboot /usr/local/bin/backrest") | crontab - + ``` + +3. **Verify Installation** + - Access Backrest at `http://localhost:9898` + - For the systemd service: `sudo systemctl status backrest` + +> [!NOTE] +> Adjust the `User` in the systemd service file if needed. The install script and manual systemd instructions use your current user by default. +> +> By default backrest listens only on localhost, you can open optionally open it up to remote connections by setting the `BACKREST_PORT` environment variable. For systemd installations, run `sudo systemctl edit backrest` and add: +> ``` +> [Service] +> Environment="BACKREST_PORT=0.0.0.0:9898" +> ``` +> Using `0.0.0.0` allows connections from any interface. + +### Arch Linux + +> [!Note] +> [Backrest on AUR](https://aur.archlinux.org/packages/backrest) is not maintained by the Backrest official and has made minor adjustments to the recommended services. Please refer to [here](https://aur.archlinux.org/cgit/aur.git/tree/backrest@.service?h=backrest) for details. In [backrest@.service](https://aur.archlinux.org/cgit/aur.git/tree/backrest@.service?h=backrest), use `restic` from the Arch Linux official repository by setting `BACKREST_RESTIC_COMMAND`. And for information on enable/starting/stopping services, please refer to [Systemd#Using_units](https://wiki.archlinux.org/title/Systemd#Using_units). + +```shell +## Install Backrest from AUR +paru -Sy backrest # or: yay -Sy backrest + +## Enable Backrest service for current user +sudo systemctl enable --now backrest@$USER.service +``` + +## Running on macOS + +### Using Homebrew (Recommended) + +Backrest is available via a [Homebrew tap](https://github.com/garethgeorge/homebrew-backrest-tap): + +```sh +brew tap garethgeorge/homebrew-backrest-tap +brew install backrest +brew services start backrest +``` + +This method uses [Brew Services](https://github.com/Homebrew/homebrew-services) to manage Backrest. It will launch on startup and run on port 127.0.0.1:9898 by default. + +> [!NOTE] +> You may need to grant Full Disk Access to Backrest. Go to `System Preferences > Security & Privacy > Privacy > Full Disk Access` and add `/usr/local/bin/backrest`. + +### Manual Installation + +1. Download the latest Darwin release from the [releases page](https://github.com/garethgeorge/backrest/releases). +2. Extract and install: + +```sh +mkdir backrest && tar -xzvf backrest_Darwin_arm64.tar.gz -C backrest +cd backrest && ./install.sh +``` + +The install script will: +- Move the Backrest binary to `/usr/local/bin` +- Create a launch agent at `~/Library/LaunchAgents/com.backrest.plist` +- Load the launch agent + +> [!TIP] +> Review the script before running to ensure you're comfortable with its operations. + +## Running on Windows + +#### Windows Installer + +Download the Windows installer for your architecture from the [releases page](https://github.com/garethgeorge/backrest/releases). The installer, named Backrest-setup-[arch].exe, will place Backrest and a GUI tray application in `%localappdata%\Programs\Backrest\`. The tray application, set to start on login, monitors Backrest. + +> [!TIP] +> To override the default port before installation, set a user environment variable named BACKREST_PORT. On Windows 10+, navigate to Settings > About > Advanced system settings > Environment Variables. Under "User variables", create a new variable with the value "127.0.0.1:port" (e.g., "127.0.0.1:8080" for port 8080). If changing post-installation, re-run the installer to update shortcuts with the new port. + +# Configuration + +## Environment Variables (Unix) + +| Variable | Description | Default | +| ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `BACKREST_PORT` | Port to bind to | 127.0.0.1:9898 (or 0.0.0.0:9898 for the docker images) | +| `BACKREST_CONFIG` | Path to config file | `$HOME/.config/backrest/config.json`
(or, if `$XDG_CONFIG_HOME` is set, `$XDG_CONFIG_HOME/backrest/config.json`) | +| `BACKREST_DATA` | Path to the data directory | `$HOME/.local/share/backrest`
(or, if `$XDG_DATA_HOME` is set, `$XDG_DATA_HOME/backrest`) | +| `BACKREST_RESTIC_COMMAND` | Path to restic binary | Defaults to a Backrest managed version of restic at `$XDG_DATA_HOME/backrest/restic-x.x.x` | +| `XDG_CACHE_HOME` | Path to the cache directory | | + +## Environment Variables (Windows) + +| Variable | Description | Default | +| ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------ | +| `BACKREST_PORT` | Port to bind to | 127.0.0.1:9898 | +| `BACKREST_CONFIG` | Path to config file | `%appdata%\backrest` | +| `BACKREST_DATA` | Path to the data directory | `%appdata%\backrest\data` | +| `BACKREST_RESTIC_COMMAND` | Path to restic binary | Defaults to a Backrest managed version of restic in `C:\Program Files\restic\restic-x.x.x` | +| `XDG_CACHE_HOME` | Path to the cache directory | | + +# Contributing + +Contributions are welcome! See the [issues](https://github.com/garethgeorge/backrest/issues) or feel free to open a new issue to discuss a project. Beyond the core codebase, contributions to [documentation](https://garethgeorge.github.io/backrest/introduction/getting-started), [cookbooks](https://garethgeorge.github.io/backrest/cookbooks/command-hook-examples), and testing are always welcome. + +## Build Depedencies + +- [Node.js](https://nodejs.org/en) for UI development +- [Go](https://go.dev/) 1.21 or greater for server development +- [goreleaser](https://github.com/goreleaser/goreleaser) `go install github.com/goreleaser/goreleaser@latest` + +**(Optional) To Edit Protobuffers** ```sh apt install -y protobuf-compiler -go install \ - github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest \ - github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest -go install github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts@latest -go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest -go install github.com/bufbuild/buf/cmd/buf@v1.27.2 +go install github.com/bufbuild/buf/cmd/buf@v1.47.2 +go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest +npm install -g @bufbuild/protoc-gen-es +``` + +## Compiling + +```sh +(cd webui && npm i && npm run build) +(cd cmd/backrest && go build .) ``` + +## Using VSCode Dev Containers + +You can also use VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension to quickly get up and running with a working development and debugging environment. + +0. Make sure Docker and VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension is installed +1. Clone this repository +2. Open this folder in VSCode +3. When prompted, click on `Open in Container` button, or run `> Dev Containers: Rebuild and Reopen in Containers` command +4. When container is started, go to `Run and Debug`, choose `Debug Backrest (backend+frontend)` and run it + +> [!NOTE] +> Provided launch configuration has hot reload for typescript frontend. diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 000000000..6a4440a3b Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/install.nsi b/build/windows/install.nsi new file mode 100644 index 000000000..9bea917a6 --- /dev/null +++ b/build/windows/install.nsi @@ -0,0 +1,303 @@ +!define BUILD_DIR "." +!define OUT_DIR "." +!define APP_NAME "Backrest" +!define COMP_NAME "garethgeorge" +!define WEB_SITE "https://github.com/garethgeorge/backrest" +!define COPYRIGHT "garethgeorge 2024" +!define DESCRIPTION "${APP_NAME} installer" +!define LICENSE_TXT "${BUILD_DIR}\LICENSE" +!define MAIN_APP_EXE "backrest-windows-tray.exe" +!define INSTALL_TYPE "SetShellVarContext current" +!define REG_ROOT "HKCU" +!define REG_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" +# Extract version from the changelog. +!searchparse /file "${BUILD_DIR}\CHANGELOG.md" `## [` VERSION `]` +# User variables. +Var UIPort +Var WelcomeTitle +Var WelcomeText +Var WelcomePortNote +Var OldVersion +Var Cmd +Var InstallMode +Var InstallModeLower + +###################################################################### +# Installer file properties +# NSIS requires X.X.X.X format in VIProductVersion. +VIProductVersion "${VERSION}.0" +VIAddVersionKey "ProductName" "${APP_NAME}" +VIAddVersionKey "CompanyName" "${COMP_NAME}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileDescription" "${DESCRIPTION}" +VIAddVersionKey "FileVersion" "${VERSION}" +VIAddVersionKey "ProductVersion" "${VERSION}" + +###################################################################### +# Installer settings +Unicode True +RequestExecutionLevel user +SetCompressor LZMA +Name "${APP_NAME}" +Caption "$(^Name) ${VERSION} Setup" +!ifdef ARCH +OutFile "${OUT_DIR}\Backrest-${ARCH}-setup.exe" +!else +OutFile "${OUT_DIR}\Backrest-setup.exe" +!endif +XPStyle on +# Default installation directory. +InstallDir "$LOCALAPPDATA\Programs\Backrest" +# If existing installation is detected, use that directory instead. +InstallDirRegKey "${REG_ROOT}" "${REG_UNINSTALL_PATH}" "UninstallString" +ManifestDPIAware true +ShowInstDetails show +ShowUninstDetails show +# Include NSIS headers used by this script. +!include "MUI2.nsh" +!include "LogicLib.nsh" +!include "Memento.nsh" +!include "WordFunc.nsh" +# Defines for the Memento macro. +!define MEMENTO_REGISTRY_ROOT "${REG_ROOT}" +!define MEMENTO_REGISTRY_KEY "${REG_UNINSTALL_PATH}" + +###################################################################### +# GUI pages +# Prompt to confirm exiting the installer. +!define MUI_ABORTWARNING +!define MUI_UNABORTWARNING + +!define MUI_WELCOMEPAGE_TITLE "$WelcomeTitle" +!define MUI_TEXT_WELCOME_INFO_TEXT "$WelcomeText" +!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreWelcome +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE onLeaveWelcome +!insertmacro MUI_PAGE_WELCOME + +!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}" + +!define MUI_COMPONENTSPAGE_NODESC +!define MUI_COMPONENTSPAGE_TEXT_COMPLIST "Select components to install:$\r$\n$\r$\nSelections will be remembered for future upgrades" +!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreComponents +!insertmacro MUI_PAGE_COMPONENTS + +!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreDirectory +!insertmacro MUI_PAGE_DIRECTORY + +!insertmacro MUI_PAGE_INSTFILES + +!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAIN_APP_EXE}" +!define MUI_FINISHPAGE_RUN_TEXT "&Start ${APP_NAME} (runs in the system tray)" +# Use the built-in readme option to open the app URL. +!define MUI_FINISHPAGE_SHOWREADME http://localhost:$UIPort/ +!define MUI_FINISHPAGE_SHOWREADME_TEXT "&Open Backrest user interface" +!define MUI_PAGE_CUSTOMFUNCTION_SHOW onShowFinish +!insertmacro MUI_PAGE_FINISH + +# Uninstall pages. +!define MUI_UNFINISHPAGE_NOAUTOCLOSE +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_UNPAGE_FINISH + +!insertmacro MUI_LANGUAGE "English" + +###################################################################### +# Functions +# Have to define the function this way to allow re-using it in the uninstall section. +!macro KillProcess UN +Function ${UN}KillProcess +ReadEnvStr $Cmd COMSPEC +DetailPrint "Stopping Backrest if it is running..." +# Gracefully attempt to stop Backrest processes for the current user. +# Do it 5 times, then kill forcefully. +nsExec::ExecToLog '$Cmd /C echo off & (for /L %i in (1,1,5) do tasklist /FI "USERNAME eq %USERNAME%" | findstr /I /V "setup" | findstr "backrest" && taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" || exit) & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" /F & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F ' +FunctionEnd +!macroend +!insertmacro KillProcess "" +!insertmacro KillProcess "un." + +Function .onInit +# $R0, $R1 etc are registers; used here as local variables. +# Read some environment variables. +ReadEnvStr $Cmd COMSPEC +ReadEnvStr $R1 BACKREST_PORT +${If} "$R1" == "" + # Use the default port and welcome text if the var is empty. + StrCpy $UIPort "9898" + StrCpy $WelcomePortNote "" +${Else} + # Extract port number. + ${WordFind} "$R1" ":" "+2" $UIPort + StrCpy $WelcomePortNote "$\r$\n$\r$\nNOTE: detected BACKREST_PORT environment variable. Will use port $UIPort for shortcuts." +${EndIf} + +# Read the previous Backrest version, if any. +ReadRegStr $OldVersion ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayVersion" +${If} "$OldVersion" == "00.00.00.00" + # Old pre-1.6.2 installer installed into C:\Program Files; override the default path when upgrading. + StrCpy $INSTDIR "$LOCALAPPDATA\Programs\Backrest" +${EndIf} + +${If} "$OldVersion" != "" + # Detected existing installation. + ${MementoSectionRestore} + ${VersionCompare} "$OldVersion" "${VERSION}" $R3 + ${Select} $R3 + ${Case} "0" + StrCpy $InstallMode "Reinstall" + ${Case} "1" + StrCpy $InstallMode "Downgrade" + ${CaseElse} + StrCpy $InstallMode "Upgrade" + ${EndSelect} + StrCpy $WelcomeTitle "Welcome to ${APP_NAME} $InstallMode" + # Convert to lowercase for Welcome text. + ${StrFilter} "$InstallMode" "-" "" "" $InstallModeLower + StrCpy $WelcomeText "Setup will guide you through the $InstallModeLower of ${APP_NAME} from version $OldVersion to ${VERSION}.$\r$\n$\r$\nInstallation directory is $INSTDIR $WelcomePortNote$\r$\n$\r$\nClick Next to continue." +${Else} + # New installation. + # Check if port is already in use and go into the abort mode. + nsExec::ExecToStack '$Cmd /C netstat.exe -na | findstr LISTENING | findstr ":$UIPort " ' + Pop $R4 + ${If} "$R4" == "0" + StrCpy "$InstallMode" "Abort" + StrCpy $WelcomeTitle "Error" + StrCpy $WelcomeText "*** WARNING ***$\r$\nBackrest binds to port $UIPort for web UI. This port is currently in use by another Backrest instance or another application.$\r$\n$\r$\nPerform the following:$\r$\nClick Start - type $\"environment$\", Enter to open System Properties.$\r$\nClick Environment Variables. Click New in the top section. Enter BACKREST_PORT as the name and 127.0.0.1:port as the value, where $\"port$\" is a number between 1024 and 65535 (avoid known ports; try 9900), then OK 3 times.$\r$\nExit and re-run this installer to have it pick up the new value.$\r$\nSee installation documentation for more details.$\r$\n$\r$\nClick Exit to exit." + ${Else} + StrCpy $WelcomeTitle "Welcome to ${APP_NAME} Setup" + StrCpy $WelcomeText "Setup will guide you through the installation of ${APP_NAME}.$WelcomePortNote$\r$\n$\r$\nClick Next to continue." + ${EndIf} +${EndIf} +FunctionEnd + +Function onPreWelcome + ${If} "$InstallMode" == "Abort" + # Change text on the button. + GetDlgItem $R5 $HWNDPARENT 1 + ${NSD_SetText} $R5 "&Exit" + ${EndIf} +FunctionEnd + +Function onLeaveWelcome + ${If} "$InstallMode" == "Abort" + Quit + ${EndIf} +FunctionEnd + +Function onPreComponents + ${If} "$InstallMode" != "" + GetDlgItem $R6 $HWNDPARENT 1 + ${NSD_SetText} $R6 "$(^InstallBtn)" + ${EndIf} +FunctionEnd + +Function onPreDirectory + # Skip directory page. + ${If} "$InstallMode" != "" + Abort + ${EndIf} +FunctionEnd + +Function onShowFinish + # Run custom functions when the checkboxes are clicked. + ${NSD_OnClick} $mui.FinishPage.Run onChkRun +FunctionEnd + +Function onChkRun + Pop $R7 + ${NSD_GetState} $mui.FinishPage.Run $7 + ${If} $7 == ${BST_UNCHECKED} + ${NSD_Uncheck} $mui.FinishPage.ShowReadme + EnableWindow $mui.FinishPage.ShowReadme 0 + ${Else} + EnableWindow $mui.FinishPage.ShowReadme 1 + ${EndIf} +FunctionEnd + +Function .onInstSuccess + ${MementoSectionSave} +FunctionEnd + +###################################################################### +# Sections +Section "Application files" +SectionIn RO +${INSTALL_TYPE} +Call KillProcess +# Clean up remnants from the old installer (except for items in "Program Files" which would require elevation). +${If} "$OldVersion" == "00.00.00.00" + Delete "$DESKTOP\${APP_NAME} Console.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\Uninstall ${APP_NAME}.lnk" + DeleteRegKey ${REG_ROOT} "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}" +${EndIf} + +# Allow reinstall and downgrade by overwriting the files. +SetOverwrite on +SetOutPath "$INSTDIR" +File "${BUILD_DIR}\backrest.exe" +File "${BUILD_DIR}\backrest-windows-tray.exe" +File "${BUILD_DIR}\LICENSE" +File "${BUILD_DIR}\icon.ico" +WriteUninstaller "$INSTDIR\uninstall.exe" + +# Start Menu shortcuts. +CreateDirectory "$SMPROGRAMS\${APP_NAME}" +CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\icon.ico" 0 +CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} UI.lnk" "http://localhost:$UIPort/" "" "$INSTDIR\icon.ico" 0 +WriteIniStr "$SMPROGRAMS\${APP_NAME}\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}" + +# Registry entries. +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayName" "${APP_NAME}" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\icon.ico" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}" +WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "InstallLocation" "$INSTDIR" +SectionEnd + +${MementoSection} "Run application at startup (recommended)" sect_startup +CreateDirectory $SMSTARTUP +CreateShortcut "$SMSTARTUP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\icon.ico" 0 +${MementoSectionEnd} + +${MementoSection} "Desktop shortcut" sect_desktop +CreateShortCut "$DESKTOP\${APP_NAME} UI.lnk" "http://localhost:$UIPort/" "" "$INSTDIR\icon.ico" 0 +${MementoSectionEnd} +${MementoSectionDone} + +# If a previous installation created the shortcuts, remove them when user deselects +# upon upgrade/reinstall to honour the new choice. +Section "-Remove deselected shortcuts" +${IfNot} ${SectionIsSelected} ${sect_startup} + Delete "$SMSTARTUP\${APP_NAME}.lnk" +${EndIf} +${IfNot} ${SectionIsSelected} ${sect_desktop} + Delete "$DESKTOP\${APP_NAME} UI.lnk" +${EndIf} +SectionEnd + +Section "Uninstall" +${INSTALL_TYPE} +Call un.KillProcess +Delete "$INSTDIR\LICENSE" +Delete "$INSTDIR\icon.ico" +Delete "$INSTDIR\install.lock" +Delete "$INSTDIR\restic*.exe" +Delete "$INSTDIR\backrest.exe" +Delete "$INSTDIR\backrest-windows-tray.exe" +Delete "$INSTDIR\uninstall.exe" +RmDir "$INSTDIR" +# Delete Start Menu shortcuts. +Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} UI.lnk" +Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} website.url" +RmDir "$SMPROGRAMS\${APP_NAME}" +# Startup and desktop shortcuts. +Delete "$SMSTARTUP\${APP_NAME}.lnk" +Delete "$DESKTOP\${APP_NAME} UI.lnk" +# Registry key. +DeleteRegKey ${REG_ROOT} "${REG_UNINSTALL_PATH}" +SectionEnd diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go new file mode 100644 index 000000000..8fc4f4486 --- /dev/null +++ b/cmd/backrest/backrest.go @@ -0,0 +1,344 @@ +package main + +import ( + "context" + "crypto/rand" + "errors" + "flag" + "net/http" + "os" + "os/signal" + "path" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "syscall" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + "github.com/garethgeorge/backrest/internal/api" + syncapi "github.com/garethgeorge/backrest/internal/api/syncapi" + "github.com/garethgeorge/backrest/internal/auth" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/env" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/metric" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" + "github.com/garethgeorge/backrest/internal/orchestrator" + "github.com/garethgeorge/backrest/internal/resticinstaller" + "github.com/garethgeorge/backrest/webui" + "github.com/mattn/go-colorable" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "gopkg.in/natefinch/lumberjack.v2" +) + +var InstallDepsOnly = flag.Bool("install-deps-only", false, "install dependencies and exit") +var ( + version = "unknown" + commit = "unknown" +) + +func main() { + flag.Parse() + installLoggers() + + resticPath, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + zap.S().Fatalf("error finding or installing restic: %v", err) + } + + if *InstallDepsOnly { + zap.S().Info("dependencies installed, exiting") + return + } + + ctx, cancel := context.WithCancel(context.Background()) + go onterm(os.Interrupt, cancel) + go onterm(os.Interrupt, newForceKillHandler()) + + // Load the configuration + configStore := createConfigProvider() + cfg, err := configStore.Get() + if err != nil { + zap.S().Fatalf("error loading config: %v", err) + } + configMgr := &config.ConfigManager{Store: configStore} + + var wg sync.WaitGroup + + // Create / load the operation log + oplogFile := path.Join(env.DataDir(), "oplog.sqlite") + opstore, err := sqlitestore.NewSqliteStore(oplogFile) + if errors.Is(err, sqlitestore.ErrLocked) { + zap.S().Fatalf("oplog is locked by another instance of backrest that is using the same data directory %q, kill that instance before starting another one.", env.DataDir()) + } else if err != nil { + zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) + zap.S().Fatalf("error creating oplog: %v", err) + } + defer opstore.Close() + + log, err := oplog.NewOpLog(opstore) + if err != nil { + zap.S().Fatalf("error creating oplog: %v", err) + } + migrateBboltOplog(opstore) + migratePopulateGuids(opstore, cfg) + + // Create rotating log storage + logStore, err := logstore.NewLogStore(filepath.Join(env.DataDir(), "tasklogs")) + if err != nil { + zap.S().Fatalf("error creating task log store: %v", err) + } + logstore.MigrateTarLogsInDir(logStore, filepath.Join(env.DataDir(), "rotatinglogs")) + deleteLogsForOp := func(ops []*v1.Operation, event oplog.OperationEvent) { + if event != oplog.OPERATION_DELETED { + return + } + for _, op := range ops { + if err := logStore.DeleteWithParent(op.Id); err != nil { + zap.S().Warnf("error deleting logs for operation %q: %v", op.Id, err) + } + } + } + log.Subscribe(oplog.Query{}, &deleteLogsForOp) + defer func() { + if err := logStore.Close(); err != nil { + zap.S().Warnf("error closing log store: %v", err) + } + log.Unsubscribe(&deleteLogsForOp) + }() + + // Create orchestrator and start task loop. + orchestrator, err := orchestrator.NewOrchestrator(resticPath, configMgr, log, logStore) + if err != nil { + zap.S().Fatalf("error creating orchestrator: %v", err) + } + + wg.Add(1) + go func() { + orchestrator.Run(ctx) + wg.Done() + }() + + // Create and serve the HTTP gateway + remoteConfigStore := syncapi.NewJSONDirRemoteConfigStore(filepath.Join(env.DataDir(), "sync", "remote_configs")) + syncMgr := syncapi.NewSyncManager(configMgr, remoteConfigStore, log, orchestrator) + wg.Add(1) + go func() { + syncMgr.RunSync(ctx) + wg.Done() + }() + + syncHandler := syncapi.NewBackrestSyncHandler(syncMgr) + + apiBackrestHandler := api.NewBackrestHandler( + configMgr, + remoteConfigStore, + orchestrator, + log, + logStore, + ) + authenticator := auth.NewAuthenticator(getSecret(), configMgr) + apiAuthenticationHandler := api.NewAuthenticationHandler(authenticator) + + mux := http.NewServeMux() + mux.Handle(v1connect.NewAuthenticationHandler(apiAuthenticationHandler)) + if cfg.GetMultihost() != nil { + // alpha feature, only available if the user manually enables it in the config. + mux.Handle(v1connect.NewBackrestSyncServiceHandler(syncHandler)) + } + backrestHandlerPath, backrestHandler := v1connect.NewBackrestHandler(apiBackrestHandler) + mux.Handle(backrestHandlerPath, auth.RequireAuthentication(backrestHandler, authenticator)) + mux.Handle("/", webui.Handler()) + mux.Handle("/download/", http.StripPrefix("/download", api.NewDownloadHandler(log))) + mux.Handle("/metrics", auth.RequireAuthentication(metric.GetRegistry().Handler(), authenticator)) + + // Serve the HTTP gateway + server := &http.Server{ + Addr: env.BindAddress(), + Handler: h2c.NewHandler(mux, &http2.Server{}), // h2c is HTTP/2 without TLS for grpc-connect support. + } + + zap.S().Infof("starting web server %v", server.Addr) + go func() { + <-ctx.Done() + server.Shutdown(context.Background()) + }() + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + zap.L().Error("error starting server", zap.Error(err)) + } + zap.L().Info("HTTP gateway shutdown") + + wg.Wait() +} + +func createConfigProvider() config.ConfigStore { + return &config.CachingValidatingStore{ + ConfigStore: &config.JsonFileStore{Path: env.ConfigFilePath()}, + } +} + +func onterm(s os.Signal, callback func()) { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, s, syscall.SIGTERM) + for { + <-sigchan + callback() + } +} + +func getSecret() []byte { + secretFile := path.Join(env.DataDir(), "jwt-secret") + data, err := os.ReadFile(secretFile) + if err == nil { + zap.L().Debug("loading auth secret from file") + return data + } + + zap.L().Info("generating new auth secret") + secret := make([]byte, 64) + if n, err := rand.Read(secret); err != nil || n != 64 { + zap.S().Fatalf("error generating secret: %v", err) + } + if err := os.MkdirAll(env.DataDir(), 0700); err != nil { + zap.S().Fatalf("error creating data directory: %v", err) + } + if err := os.WriteFile(secretFile, secret, 0600); err != nil { + zap.S().Fatalf("error writing secret to file: %v", err) + } + return secret +} + +func newForceKillHandler() func() { + var times atomic.Int32 + return func() { + if times.Load() > 0 { + buf := make([]byte, 1<<16) + runtime.Stack(buf, true) + os.Stderr.Write(buf) + zap.S().Fatal("dumped all running coroutine stack traces, forcing termination") + } + times.Add(1) + zap.S().Warn("attempting graceful shutdown, to force termination press Ctrl+C again") + } +} + +func installLoggers() { + // Pretty logging for console + c := zap.NewDevelopmentEncoderConfig() + c.EncodeLevel = zapcore.CapitalColorLevelEncoder + c.EncodeTime = zapcore.ISO8601TimeEncoder + + debugLevel := zapcore.InfoLevel + if version == "unknown" { // dev build + debugLevel = zapcore.DebugLevel + } + pretty := zapcore.NewCore( + zapcore.NewConsoleEncoder(c), + zapcore.AddSync(colorable.NewColorableStdout()), + debugLevel, + ) + + // JSON logging to log directory + logsDir := env.LogsPath() + if err := os.MkdirAll(logsDir, 0755); err != nil { + zap.ReplaceGlobals(zap.New(pretty)) + zap.S().Errorf("error creating logs directory %q, will only log to console for now: %v", err) + return + } + + writer := &lumberjack.Logger{ + Filename: filepath.Join(logsDir, "backrest.log"), + MaxSize: 5, // megabytes + MaxBackups: 3, + MaxAge: 14, + Compress: true, + } + + ugly := zapcore.NewCore( + zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), + zapcore.AddSync(writer), + zapcore.DebugLevel, + ) + + zap.ReplaceGlobals(zap.New(zapcore.NewTee(pretty, ugly))) + zap.S().Infof("backrest version %v@%v, using log directory: %v", version, commit, logsDir) +} + +// migrateBboltOplog migrates the old bbolt oplog to the new sqlite oplog. +// It is careful to ensure that all migrations are applied before copying +// operations directly to the sqlite logstore. +func migrateBboltOplog(logstore oplog.OpStore) { + oldBboltOplogFile := path.Join(env.DataDir(), "oplog.boltdb") + if _, err := os.Stat(oldBboltOplogFile); err != nil { + return + } + + zap.S().Warnf("found old bbolt oplog file %q, migrating to sqlite", oldBboltOplogFile) + oldOpstore, err := bboltstore.NewBboltStore(oldBboltOplogFile) + if err != nil { + zap.S().Fatalf("error opening old bolt opstore: %v", oldBboltOplogFile, err) + } + oldOplog, err := oplog.NewOpLog(oldOpstore) + if err != nil { + zap.S().Fatalf("error opening old bolt oplog: %v", oldBboltOplogFile, err) + } + + var errs []error + var count int + if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { + if err := logstore.Add(op); err != nil { + errs = append(errs, err) + zap.L().Warn("failed to migrate operation", zap.Error(err), zap.Any("operation", op)) + } else { + count++ + } + return nil + }); err != nil { + zap.S().Warnf("couldn't migrate all operations from the old bbolt oplog, if this recurs delete the file %q and restart", oldBboltOplogFile) + zap.S().Fatalf("error migrating old bbolt oplog: %v", err) + } + + if len(errs) > 0 { + zap.S().Errorf("encountered %d errors migrating old bbolt oplog, see logs for details.", len(errs), oldBboltOplogFile) + } + if err := oldOpstore.Close(); err != nil { + zap.S().Warnf("error closing old bbolt oplog: %v", err) + } + if err := os.Rename(oldBboltOplogFile, oldBboltOplogFile+".deprecated"); err != nil { + zap.S().Warnf("error removing old bbolt oplog: %v", err) + } + zap.S().Infof("migrated %d operations from old bbolt oplog to sqlite", count) +} + +func migratePopulateGuids(logstore oplog.OpStore, cfg *v1.Config) { + repoToGUID := make(map[string]string) + for _, repo := range cfg.Repos { + if repo.Guid != "" { + repoToGUID[repo.Id] = repo.Guid + } + } + + migratedOpCount := 0 + if err := logstore.Transform(oplog.Query{}.SetRepoGUID(""), func(op *v1.Operation) (*v1.Operation, error) { + if op.RepoGuid != "" { + return nil, nil + } + if guid, ok := repoToGUID[op.RepoId]; ok { + op.RepoGuid = guid + migratedOpCount++ + return op, nil + } + return nil, nil + }); err != nil { + zap.S().Fatalf("error populating repo GUIDs for existing operations: %v", err) + } else if migratedOpCount > 0 { + zap.S().Infof("populated repo GUIDs for %d existing operations", migratedOpCount) + } +} diff --git a/cmd/backrestmon/backrestmon.go b/cmd/backrestmon/backrestmon.go new file mode 100644 index 000000000..cc85821e6 --- /dev/null +++ b/cmd/backrestmon/backrestmon.go @@ -0,0 +1,141 @@ +//go:build windows +// +build windows + +package main + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "syscall" + + "github.com/garethgeorge/backrest/internal/env" + "github.com/getlantern/systray" + "github.com/ncruces/zenity" + + _ "embed" +) + +//go:embed icon.ico +var icon []byte + +func main() { + backrest, err := findBackrest() + if err != nil { + reportError(err) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + + cmd := exec.CommandContext(ctx, backrest) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "ENV=production") + + if err := cmd.Start(); err != nil { + reportError(err) + cancel() + return + } + + systray.Run(func() { + systray.SetTitle("Backrest Tray") + systray.SetTooltip("Manage backrest") + systray.SetIcon(icon) + + // First item: open the WebUI in the default browser. + mOpenUI := systray.AddMenuItem("Open WebUI", "Open the Backrest WebUI in your default browser") + mOpenUI.ClickedCh = make(chan struct{}) + go func() { + for range mOpenUI.ClickedCh { + bindaddr := env.BindAddress() + if bindaddr == "" { + bindaddr = ":9898" + } + _, port, err := net.SplitHostPort(bindaddr) + if err != nil { + port = "9898" // try the default + } + if err := openBrowser(fmt.Sprintf("http://localhost:%v", port)); err != nil { + reportError(err) + } + } + }() + + // Second item: open the log file in the file explorer + mOpenLog := systray.AddMenuItem("Open Log Dir", "Open the Backrest log directory") + mOpenLog.ClickedCh = make(chan struct{}) + go func() { + for range mOpenLog.ClickedCh { + cmd := exec.Command(`explorer`, `/select,`, env.LogsPath()) + cmd.Start() + go cmd.Wait() + } + }() + + // Last item: quit button to stop the backrest process. + mQuit := systray.AddMenuItem("Quit", "Kills the backrest process and exits the tray app") + mQuit.ClickedCh = make(chan struct{}) + go func() { + <-mQuit.ClickedCh + cancel() + systray.Quit() + }() + }, func() { + cancel() + }) + + if err := cmd.Wait(); err != nil { + systray.Quit() + if ctx.Err() != context.Canceled { + reportError(fmt.Errorf("backrest process exited unexpectedly with error: %w", err)) + } + return + } +} + +func findBackrest() (string, error) { + // Backrest binary must be installed in the same directory as the backresttray binary. + ex, err := os.Executable() + if err != nil { + return "", err + } + dir := filepath.Dir(ex) + + wantPath := filepath.Join(dir, backrestBinName()) + + if stat, err := os.Stat(wantPath); err == nil && !stat.IsDir() { + return wantPath, nil + } + return "", fmt.Errorf("backrest binary not found at %s", wantPath) +} + +func backrestBinName() string { + if runtime.GOOS == "windows" { + return "backrest.exe" + } else { + return "backrest" + } +} + +func openBrowser(url string) error { + switch runtime.GOOS { + case "linux": + return exec.Command("xdg-open", url).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + return exec.Command("open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } +} + +func reportError(err error) { + zenity.Error(err.Error(), zenity.Title("Backrest Error")) +} diff --git a/cmd/backrestmon/icon.ico b/cmd/backrestmon/icon.ico new file mode 100644 index 000000000..6a4440a3b Binary files /dev/null and b/cmd/backrestmon/icon.ico differ diff --git a/cmd/resticui/resticui.go b/cmd/resticui/resticui.go deleted file mode 100644 index 3f83f2b64..000000000 --- a/cmd/resticui/resticui.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "context" - "errors" - "net/http" - "os" - "os/signal" - "sync" - "syscall" - - "github.com/garethgeorge/resticui/internal/api" - "github.com/garethgeorge/resticui/static" - "go.uber.org/zap" - - _ "embed" -) - -func main() { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - go onterm(cancel) - - var wg sync.WaitGroup - - // Configure the HTTP mux - mux := http.NewServeMux() - mux.Handle("/", http.FileServer(http.FS(static.FS))) - - server := &http.Server{ - Addr: ":9090", - Handler: mux, - } - - // Serve the API - wg.Add(1) - go func() { - defer wg.Done() - err := api.ServeAPI(ctx, mux) - if err != nil { - zap.S().Fatal("Error serving API", zap.Error(err)) - } - cancel() // cancel the context when the API server exits (e.g. on fatal error) - }() - - // Serve the HTTP gateway - wg.Add(1) - go func() { - defer wg.Done() - zap.S().Infof("HTTP binding to address %v", server.Addr) - go func() { - <-ctx.Done() - server.Shutdown(context.Background()) - }() - if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - zap.S().Error("Error starting server", zap.Error(err)) - } - zap.S().Info("HTTP gateway shutdown") - cancel() // cancel the context when the HTTP server exits (e.g. on fatal error) - }() - - wg.Wait() -} - -func init() { - zap.ReplaceGlobals(zap.Must(zap.NewProduction())) - if os.Getenv("DEBUG") != "" { - zap.ReplaceGlobals(zap.Must(zap.NewDevelopmentConfig().Build())) - } -} - -func onterm(callback func()) { - sigchan := make(chan os.Signal, 1) - signal.Notify(sigchan, os.Interrupt, syscall.SIGTERM) - <-sigchan - callback() -} \ No newline at end of file diff --git a/docs/.eslintrc.cjs b/docs/.eslintrc.cjs new file mode 100644 index 000000000..45b86ae35 --- /dev/null +++ b/docs/.eslintrc.cjs @@ -0,0 +1,9 @@ +module.exports = { + root: true, + extends: ["@nuxt/eslint-config"], + ignorePatterns: ["dist", "node_modules", ".output", ".nuxt"], + rules: { + "vue/max-attributes-per-line": "off", + "vue/multi-word-component-names": "off", + }, +}; diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100755 index 000000000..69f6b69d0 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,12 @@ +node_modules +*.iml +.idea +*.log* +.nuxt +.vscode +.DS_Store +coverage +dist +sw.* +.env +.output diff --git a/docs/.npmrc b/docs/.npmrc new file mode 100644 index 000000000..cf0404245 --- /dev/null +++ b/docs/.npmrc @@ -0,0 +1,2 @@ +shamefully-hoist=true +strict-peer-dependencies=false diff --git a/docs/.prettierignore b/docs/.prettierignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/docs/.prettierignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/docs/app.config.ts b/docs/app.config.ts new file mode 100644 index 000000000..2c02cd477 --- /dev/null +++ b/docs/app.config.ts @@ -0,0 +1,33 @@ +// https://github.com/nuxt-themes/docus/blob/main/nuxt.schema.ts +export default defineAppConfig({ + docus: { + title: "Backrest", + description: "Backrest is a web UI and orchestrator for restic backup.", + // image: 'https://user-images.githubusercontent.com/904724/185365452-87b7ca7b-6030-4813-a2db-5e65c785bf88.png', + socials: { + github: "garethgeorge/backrest", + }, + github: { + dir: "docs/content", + branch: "main", + repo: "backrest", + owner: "garethgeorge", + edit: true, + }, + aside: { + level: 0, + collapsed: false, + exclude: [], + }, + main: { + padded: true, + fluid: true, + }, + header: { + logo: false, + showLinkIcon: true, + exclude: [], + fluid: true, + }, + }, +}); diff --git a/docs/content/0.index.md b/docs/content/0.index.md new file mode 100644 index 000000000..865aded4f --- /dev/null +++ b/docs/content/0.index.md @@ -0,0 +1,73 @@ +--- +title: Home +navigation: false +layout: page +main: + fluid: false +--- + +:ellipsis{right=0px width=75% blur=150px} + +::block-hero +--- +cta: + - Get started + - /introduction/getting-started +secondary: + - Open on GitHub → + - https://github.com/garethgeorge/backrest +--- + +#title +Web UI and orchestrator for [Restic](https://restic.net) backup. + +#description + +Backrest is a web-accessible backup solution built on top of [restic](https://restic.net/) and providing a WebUI which wraps the restic CLI and makes it easy to create repos, browse snapshots, and restore files. Additionally, Backrest can run in the background and take an opinionated approach to scheduling snapshots and orchestrating repo health operations. + + +#extra + ::list + - Import your existing restic repositories + - Cron scheduled backups and health operations (e.g. prune and forget) + - UI for browsing and restoring files from snapshots + - Configurable backup notifications (e.g. Discord, Slack, Shoutrrr, Gotify) + - Add shell command hooks to run before and after backup operations. + - Compatible with rclone remotes + - Cross-platform support (Linux, macOS, Windows, FreeBSD, [Docker](https://hub.docker.com/r/garethgeorge/backrest)) + - Backup to any restic supported storage (e.g. S3, B2, Azure, GCS, local, SFTP, and all [rclone remotes](https://rclone.org/)) + :: + +#support +::code-group +```bash [MacOS] +brew tap garethgeorge/homebrew-backrest-tap +brew install backrest +brew services start backrest +``` +```bash [Arch Linux] +paru -Sy backrest +sudo systemctl enable --now backrest@$USER.service +``` +```yaml [docker-compose] +version: "3.2" +services: + backrest: + image: garethgeorge/backrest + container_name: backrest + hostname: backrest + volumes: + - ./backrest/data:/data + - ./backrest/config:/config + - ./backrest/cache:/cache + environment: + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TZ=America/Los_Angeles + restart: unless-stopped + ports: + - 9898:9898 +``` +:: +:: diff --git a/docs/content/1.introduction/1.getting-started.md b/docs/content/1.introduction/1.getting-started.md new file mode 100644 index 000000000..dc330aee7 --- /dev/null +++ b/docs/content/1.introduction/1.getting-started.md @@ -0,0 +1,131 @@ +# Getting Started + +This guide will walk you through the basic steps to setup a new [Backrest](https://github.com/garethgeorge/backrest) instance. + +## Installation + +Please refer to the GitHub README for platform-specific installation instructions. + +## Core Concepts + +Before diving into configuration, let's understand some key terminology: + +- **Restic Repository**: The underlying storage location where your backup data is kept. While Backrest manages this for you, understanding this concept allows you to interact directly with your backups using the restic CLI if needed. + +- **Backrest Repository**: A configuration set that defines: + - Where your backup data is stored + - Encryption credentials + - Backup orchestration settings + - Associated hooks and options + +- **Backup Plan**: A configuration that specifies: + - What to backup + - When to create snapshots + - How long to retain backups + - When to run maintenance operations + +- **Key Operations**: + - **Backup**: Creates a new snapshot of your data + - **Forget**: Marks old snapshots for deletion (without removing data) + - **Prune**: Removes unreferenced data to free up storage space + - **Restore**: Retrieves files from a snapshot to your local system + +## Initial Setup + +::alert{type="info"} +After installation, access Backrest at `http://localhost:9898` (or your configured port). You'll need to complete the initial setup process below. +:: + +### 1. Instance Configuration + +#### Instance ID +- A unique identifier for your Backrest installation +- Used to distinguish snapshots from different Backrest instances +- **Important**: Cannot be changed after initial setup + +#### Authentication +- Set your username and password during first launch +- To reset credentials: delete the `"users"` key from: + - Linux/macOS: `~/.config/backrest/config.json` + - Windows: `%appdata%\backrest\config.json` +- Authentication can be disabled for local installations or when using an authenticating reverse proxy + +### 2. Repository Setup + +Click "Add Repo" to configure your backup storage location. You can either create a new repository or connect to an existing one. + +#### Essential Repository Settings + +1. **Repository Name** + - A human-readable identifier + - Cannot be changed after creation + +2. **Repository URI** + - Specifies the backup storage location + - Common formats: + - Backblaze B2: `b2:bucket` or `b2:bucket/prefix` + - AWS S3: `s3:bucket` or `s3:bucket/prefix` + - Google Cloud: `gs:bucket:/` or `gs:bucket:/prefix` + - SFTP: `sftp:user@host:/path/to/repo` + - Local: `/mnt/backupdisk/repo1` + - Rclone: `rclone:remote:path` (requires rclone installation) + +3. **Environment Variables** + Storage provider credentials: + - S3: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` + - B2: `B2_ACCOUNT_ID`, `B2_ACCOUNT_KEY` + - Google Cloud: `GOOGLE_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS` + +4. **Optional Flags** + Common examples: + - SFTP key: `-o sftp.args="-i /path/to/key"` + - Disable locking: `--no-lock` + - Bandwidth limits: `--limit-upload 1000`, `--limit-download 1000` + +5. **Maintenance Policies** + - **Prune Policy**: Schedule for cleaning unreferenced data + - **Check Policy**: Schedule for backup integrity verification + +::alert{type="info"} +After adding a repository, use "Index Snapshots" to import existing backups. Continue to the next section to set up your backup plan. +:: + +### 3. Backup Plan Configuration + +Create a backup plan by clicking "Add Plan" and configuring these settings: + +#### Plan Settings + +1. **Plan Name** + - Choose a descriptive, immutable name + - Recommended format: `[storage]-[content]` (e.g., `b2-documents`) + +2. **Repository** + - Select your target repository + - Cannot be changed after creation + +3. **Backup Configuration** + - **Paths**: Directories/files to backup + - **Excludes**: Patterns or paths to skip (e.g., `*node_modules*`) + +4. **Schedule** + Choose one: + - Hourly/daily intervals + - Cron expression (e.g., `0 0 * * *` for daily midnight backups) + - Clock options: + - UTC/Local: Wall-clock time + - Last Run Time: Relative to previous execution + +5. **Retention Policy** + Controls snapshot lifecycle: + - **Count-based**: Keep N most recent snapshots + - **Time-based**: Keep snapshots by age (e.g., daily for 7 days, weekly for 4 weeks) + - **None**: Manual retention management + +::alert{type="success"} +Success! Now that Backrest is configured, you can sit back and let it manage your backups. Monitor the status of your backups in the UI and restore files from snapshots as needed. +:: + +::alert{type="warning"} +Make sure to save a copy of your repository credentials and encryption keys (e.g., password) in a safe place. Losing these will prevent you from restoring your data. Consider storing your entire Backrest configuration (typically `~/.config/backrest/config.json`) in a secure location, such as a password manager or encrypted storage. +:: diff --git a/docs/content/1.introduction/_dir.yml b/docs/content/1.introduction/_dir.yml new file mode 100644 index 000000000..ff0894dab --- /dev/null +++ b/docs/content/1.introduction/_dir.yml @@ -0,0 +1,2 @@ +icon: ph:star-duotone +navigation.redirect: /introduction/getting-started diff --git a/docs/content/2.docs/1.operations.md b/docs/content/2.docs/1.operations.md new file mode 100644 index 000000000..24c279a64 --- /dev/null +++ b/docs/content/2.docs/1.operations.md @@ -0,0 +1,125 @@ +# Operations Guide + +This guide details the core operations available in Backrest and how to configure them effectively. + +## Restic Integration + +Backrest executes operations through the [restic](https://restic.net) backup tool. Each operation maps to specific restic commands with additional functionality provided by Backrest. + +### Binary Management +- **Location**: Backrest searches for restic in: + 1. Data directory (typically `~/.local/share/backrest`) + 2. `/bin/` directory +- **Naming**: Binary must be named `restic-{VERSION}` +- **Auto-download**: If no binary is found, Backrest downloads a verified version from [GitHub releases](https://github.com/restic/restic/releases) +- **Verification**: Downloads are verified using SHA256 checksums signed by restic maintainers +- **Override**: Set `BACKREST_RESTIC_COMMAND` environment variable to use a custom restic binary + +### Command Execution +- **Environment**: Repository-specific environment variables are injected +- **Flags**: Repository-configured flags are appended to commands +- **Logging**: + - Error logs: Last ~500 bytes (split between start/end if longer) + - Full logs: Available via [View Logs] in the UI, truncated to 32KB (split if longer) + +## Scheduling System + +Backrest provides flexible scheduling options for all operations through policies and clocks. + +### Schedule Policies + +| Policy | Description | Use Case | +| -------------- | ------------------------------- | --------------------------------------------------------- | +| Disabled | Operation will not run | Temporarily disable operations | +| Cron | Standard cron expression timing | Precise scheduling (e.g., `0 0 * * *` for daily midnight) | +| Interval Days | Run every N days | Regular daily+ intervals | +| Interval Hours | Run every N hours | Regular sub-daily intervals | + +### Schedule Clocks + +| Clock | Description | Best For | +| ------------- | ------------------------------ | --------------------------------------- | +| Local | Local timezone wall-clock | Frequent operations (hourly+) | +| UTC | UTC timezone wall-clock | Cross-timezone coordination | +| Last Run Time | Relative to previous execution | Infrequent operations, preventing skips | + +::alert{type="info"} +**Scheduling Best Practices** +- **Backup Operations** (Plan Settings): + - Hourly or more frequent: Use "Local" clock + - Daily or less frequent: Use "Last Run Time" clock +- **Prune/Check Operations** (Repo Settings): + - Run infrequently (e.g., monthly) + - Use "Last Run Time" clock to prevent skips +:: + +## Operation Types + +### Backup +[Restic Documentation](https://restic.readthedocs.io/en/latest/040_backup.html) + +Creates snapshots of your data using `restic backup`. + +**Process Flow:** +1. **Start** + - Triggers `CONDITION_SNAPSHOT_START` hooks + - Applies hook failure policies if needed +2. **Execution** + - Runs `restic backup` + - Tags snapshot with `plan:{PLAN_ID}` and `created-by:{INSTANCE_ID}` +3. **Completion** + - Records operation metadata (files, bytes, snapshot ID) + - Triggers appropriate hooks: + - Error: `CONDITION_SNAPSHOT_ERROR` + - Success: `CONDITION_SNAPSHOT_SUCCESS` + - In both cases: `CONDITION_SNAPSHOT_END` +4. **Post-processing** + - Runs forget operation if retention policy exists + +**Snapshot Tags:** +- `plan:{PLAN_ID}`: Groups snapshots by backup plan +- `created-by:{INSTANCE_ID}`: Identifies creating Backrest instance + +### Forget +[Restic Documentation](https://restic.readthedocs.io/en/latest/060_forget.html) + +Manages snapshot retention using `restic forget --tag plan:{PLAN_ID}`. + +**Retention Policies:** +- **By Count**: `--keep-last {COUNT}` +- **By Time Period**: `--keep-{hourly,daily,weekly,monthly,yearly} {COUNT}` + +### Prune +[Restic Documentation](https://restic.readthedocs.io/en/latest/060_forget.html) + +Removes unreferenced data using `restic prune`. + +**Configuration:** +- Scheduled in repo settings +- Appears under `_system_` plan +- **Parameters:** + - Schedule timing + - Max unused percent (controls repacking threshold) + +::alert{type="info"} +**Optimization Tips:** +- Run infrequently (monthly recommended) +- Use higher max unused percent (5-10%) to reduce repacking +- Consider storage costs vs. cleanup frequency +:: + +### Check +[Restic Documentation](https://restic.readthedocs.io/en/latest/080_check.html) + +Verifies repository integrity using `restic check`. + +**Configuration:** +- Scheduled in repo settings +- Appears under `_system_` plan +- **Parameters:** + - Schedule timing + - Read data percentage + +::alert{type="warning"} +A value of 100% for *read data%* will read/download every pack file in your repository. This can be very slow and, if your provider bills for egress bandwidth, can be expensive. It is recommended to set this to 0% or a low value (e.g. 10%) for most use cases. +:: \ No newline at end of file diff --git a/docs/content/2.docs/2.hooks.md b/docs/content/2.docs/2.hooks.md new file mode 100644 index 000000000..fc370c4fe --- /dev/null +++ b/docs/content/2.docs/2.hooks.md @@ -0,0 +1,114 @@ +# Hooks + +Hooks in Backrest allow you to respond to various operation lifecycle events, enabling automation and monitoring of your backup operations. This document explains how to configure and use hooks effectively. + +## Event Types + +Hooks can be triggered by the following events: + +### Snapshot Events +- `CONDITION_SNAPSHOT_START`: Triggered when a backup operation begins and will complete before the snapshot starts. The [Error Handling](#error-handling) configuration can be used to stop the backup if the command isnt successful. +- `CONDITION_SNAPSHOT_END`: Triggered when a backup operation completes (regardless of success/failure) +- `CONDITION_SNAPSHOT_SUCCESS`: Triggered when a backup operation completes successfully +- `CONDITION_SNAPSHOT_ERROR`: Triggered when a backup operation fails +- `CONDITION_SNAPSHOT_WARNING`: Triggered when a backup operation encounters non-fatal issues + +### Prune Events +- `CONDITION_PRUNE_START`: Triggered when a prune operation begins +- `CONDITION_PRUNE_SUCCESS`: Triggered when a prune operation completes successfully +- `CONDITION_PRUNE_ERROR`: Triggered when a prune operation fails + +### Check Events +- `CONDITION_CHECK_START`: Triggered when a check operation begins +- `CONDITION_CHECK_SUCCESS`: Triggered when a check operation completes successfully +- `CONDITION_CHECK_ERROR`: Triggered when a check operation fails + +### Forget Events +- `CONDITION_FORGET_START`: Triggered when a forget operation begins +- `CONDITION_FORGET_SUCCESS`: Triggered when a forget operation completes successfully +- `CONDITION_FORGET_ERROR`: Triggered when a forget operation fails + +### General Events +- `CONDITION_ANY_ERROR`: Triggered when any operation fails + +## Notification Services + +Backrest supports multiple notification services for hook delivery: + +| Service | Description | Documentation | +| -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Discord | Send notifications to Discord channels | [Discord Webhooks Guide](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | +| Slack | Send notifications to Slack channels | [Slack Webhooks Guide](https://api.slack.com/messaging/webhooks) | +| Gotify | Send notifications via Gotify server | [Gotify Documentation](https://github.com/gotify/server) | +| Shoutrrr | Multi-provider notification service | [Shoutrrr Documentation](https://containrrr.dev/shoutrrr/v0.8/) | +| Command | Execute custom commands | See command cookbook | + +## Error Handling + +Command hooks support specific error behaviors that determine how Backrest responds to hook failures: + +- `ON_ERROR_IGNORE`: Continue execution despite hook failure +- `ON_ERROR_CANCEL`: Stop the operation but don't trigger error handlers +- `ON_ERROR_FATAL`: Stop the operation and trigger error handler hooks + +## Template System + +Hooks use Go templates for formatting notifications and scripts. The following variables and functions are available: + +### Available Variables + +| Variable | Type | Description | Example Usage | +| --------------- | ---------------------------- | --------------------------- | --------------------------------- | +| `Event` | `v1.Hook_Condition` | The triggering event | `{{ .Event }}` | +| `Task` | `string` | Task name | `{{ .Task }}` | +| `Repo` | `v1.Repo` | Repository information | `{{ .Repo.Id }}` | +| `Plan` | `v1.Plan` | Plan information | `{{ .Plan.Id }}` | +| `SnapshotId` | `string` | ID of associated snapshot | `{{ .SnapshotId }}` | +| `SnapshotStats` | `restic.BackupProgressEntry` | Backup operation statistics | See example below | +| `CurTime` | `time.Time` | Current timestamp | `{{ .FormatTime .CurTime }}` | +| `Duration` | `time.Duration` | Operation duration | `{{ .FormatDuration .Duration }}` | +| `Error` | `string` | Error message if applicable | `{{ .Error }}` | + +### Helper Functions + +| Function | Description | Example | +| ------------------ | ------------------------------- | ----------------------------------- | +| `.Summary` | Generates default event summary | `{{ .Summary }}` | +| `.FormatTime` | Formats timestamp | `{{ .FormatTime .CurTime }}` | +| `.FormatDuration` | Formats time duration | `{{ .FormatDuration .Duration }}` | +| `.FormatSizeBytes` | Formats byte sizes | `{{ .FormatSizeBytes 1048576 }}` | +| `.ShellEscape` | Escapes strings for shell usage | `{{ .ShellEscape "my string" }}` | +| `.JsonMarshal` | Converts value to JSON | `{{ .JsonMarshal .SnapshotStats }}` | + +## Default Summary Template + +Below is the implementation of the `.Summary` function, which you can use as a reference for creating custom templates: + +``` +Task: "{{ .Task }}" at {{ .FormatTime .CurTime }} +Event: {{ .EventName .Event }} +Repo: {{ .Repo.Id }} +Plan: {{ .Plan.Id }} +Snapshot: {{ .SnapshotId }} +{{ if .Error -}} +Failed to create snapshot: {{ .Error }} +{{ else -}} +{{ if .SnapshotStats -}} + +Overview: +- Data added: {{ .FormatSizeBytes .SnapshotStats.DataAdded }} +- Total files processed: {{ .SnapshotStats.TotalFilesProcessed }} +- Total bytes processed: {{ .FormatSizeBytes .SnapshotStats.TotalBytesProcessed }} + +Backup Statistics: +- Files new: {{ .SnapshotStats.FilesNew }} +- Files changed: {{ .SnapshotStats.FilesChanged }} +- Files unmodified: {{ .SnapshotStats.FilesUnmodified }} +- Dirs new: {{ .SnapshotStats.DirsNew }} +- Dirs changed: {{ .SnapshotStats.DirsChanged }} +- Dirs unmodified: {{ .SnapshotStats.DirsUnmodified }} +- Data blobs: {{ .SnapshotStats.DataBlobs }} +- Tree blobs: {{ .SnapshotStats.TreeBlobs }} +- Total duration: {{ .SnapshotStats.TotalDuration }}s +{{ end }} +{{ end }} diff --git a/docs/content/2.docs/3.api.md b/docs/content/2.docs/3.api.md new file mode 100644 index 000000000..c703f4381 --- /dev/null +++ b/docs/content/2.docs/3.api.md @@ -0,0 +1,38 @@ +# API + +Backrest provides a limited HTTP API for interacting with the backrest service. To use the API authentication must be disabled (or you can optionally provide a username and password with basic auth headers) e.g. `curl -u user:password http://localhost:9898/v1/` + +All of Backrest's API endpoints are defined as a gRPC service and are exposed over HTTP by a JSON RPC gateway for easy scripting. For the full service definition see [service.proto](https://github.com/garethgeorge/backrest/blob/main/proto/v1/service.proto). + +::alert{type="warning"} +Only the APIs documented below are considered stable, other endpoints may be subject to change. +:: + +### Backup API + +The backup API can be used to trigger execution of a plan e.g. + +``` +curl -X POST 'localhost:9898/v1.Backrest/Backup' --data '{"value": "YOUR_PLAN_ID"}' -H 'Content-Type: application/json' +``` + +The request will block until the operation has completed. A 200 response means the backup completed successfully, if the request times out the operation will continue in the background. +### Operations API + +The operations API can be used to fetch operation history e.g. + +``` +curl -X POST 'localhost:9898/v1.Backrest/GetOperations' --data '{}' -H 'Content-Type: application/json' +``` + +More complex selectors can be applied e.g. + +``` +curl -X POST 'localhost:9898/v1.Backrest/GetOperations' --data '{"selector": {"planId": "YOUR_PLAN_ID"}}' -H 'Content-Type: application/json' +``` + +For details on the structure of operations returned see the [operations.proto](https://github.com/garethgeorge/backrest/blob/main/proto/v1/operations.proto). + +::alert{type="warning"} +The structure of the operation history is subject to change over time. Different fields may be added or removed in future versions. +:: diff --git a/docs/content/2.docs/_dir.yml b/docs/content/2.docs/_dir.yml new file mode 100644 index 000000000..d8d1c6472 --- /dev/null +++ b/docs/content/2.docs/_dir.yml @@ -0,0 +1,2 @@ +title: "Docs" +icon: heroicons-outline:bookmark-alt diff --git a/docs/content/3.cookbooks/1.command-hook-examples.md b/docs/content/3.cookbooks/1.command-hook-examples.md new file mode 100644 index 000000000..3377d69b4 --- /dev/null +++ b/docs/content/3.cookbooks/1.command-hook-examples.md @@ -0,0 +1,187 @@ +# Command Hook Examples + +## Overview + +Command hooks in Backrest enable you to execute shell commands in response to specific lifecycle events, extending Backrest's functionality. This cookbook provides practical examples of command hooks for various use cases. + +## Error Behavior Configuration + +When using command hooks with `CONDITION_SNAPSHOT_START`, you can control how Backrest responds to script exit statuses: + +| Behavior | Description | +| ----------------- | ----------------------------------------------------------------------- | +| `ON_ERROR_CANCEL` | Cancels the backup operation if script exits with non-zero status | +| `ON_ERROR_FATAL` | Treats non-zero exit as backup failure and triggers error notifications | +| `ON_ERROR_IGNORE` | Continues backup operation regardless of script exit status | + +## Unix/MacOS Examples + +### Health Monitoring + +#### Healthcheck Service Integration +Notify a health monitoring service (e.g., healthchecks.io) about backup status. + +**Event:** `CONDITION_SNAPSHOT_END` +**Error Behavior:** `ON_ERROR_IGNORE` + +```bash +#!/bin/bash +{{ if .Error -}} +curl -fsS --retry 3 https://hc-ping.com/your-uuid/fail +{{ else -}} +curl -fsS --retry 3 https://hc-ping.com/your-uuid +{{ end -}} +``` + +### System Notifications + +#### MacOS System Notifications +Display system notifications for backup events. + +**Events:** `CONDITION_SNAPSHOT_END`, `CONDITION_PRUNE_ERROR`, `CONDITION_CHECK_ERROR` +**Error Behavior:** `ON_ERROR_IGNORE` + +```bash +#!/bin/bash +{{ if .Error -}} +osascript -e 'display notification "{{ .ShellEscape .Task }} failed" with title "Backrest"' +{{ else -}} +osascript -e 'display notification "{{ .ShellEscape .Task }} succeeded" with title "Backrest"' +{{ end -}} +``` + +### Pre-backup Checks + +#### Internet Connectivity Check +Verify internet connection before starting backup. + +**Event:** `CONDITION_SNAPSHOT_START` +**Error Behavior:** `ON_ERROR_CANCEL` + +```bash +#!/bin/bash +if ping -q -c 1 -W 1 google.com >/dev/null; then + echo "Internet connection is up" + exit 0 +else + echo "Internet connection is down" + exit 1 +fi +``` + +#### Directory Existence Check +Ensure backup target directory exists. + +**Event:** `CONDITION_SNAPSHOT_START` +**Error Behavior:** `ON_ERROR_CANCEL` + +```bash +#!/bin/bash +if [ -d /path/to/backup ]; then + echo "Backup directory exists" + exit 0 +else + echo "Backup directory does not exist" + exit 1 +fi +``` + +#### Battery Level Check +Verify sufficient battery level before backup. + +**Event:** `CONDITION_SNAPSHOT_START` +**Error Behavior:** `ON_ERROR_CANCEL` + +```bash +#!/bin/bash +if [ $(cat /sys/class/power_supply/BAT0/capacity) -gt 80 ]; then + echo "Battery level is above 20%" + exit 0 +else + echo "Battery level is below 20%" + exit 1 +fi +``` + +### Filesystem Operations + +#### BTRFS Snapshot Management +Create and manage BTRFS snapshots for consistent backups. + +**Pre-backup Snapshot** +**Event:** `CONDITION_SNAPSHOT_START` +**Error Behavior:** `ON_ERROR_FATAL` + +```bash +#!/bin/bash +btrfs subvolume snapshot -r /your-btrfs-filesystem /your-btrfs-filesystem/.backrest-snapshot +``` + +**Post-backup Cleanup** +**Event:** `CONDITION_SNAPSHOT_END` +**Error Behavior:** `ON_ERROR_IGNORE` + +```bash +#!/bin/bash +btrfs subvolume delete /your-btrfs-filesystem/.backrest-snapshot +``` + +## Windows Examples + +### GUI Notifications + +#### Error Message Box +Display persistent error messages requiring user acknowledgment. + +**Event:** `CONDITION_ANY_ERROR` + +```sh +Add-Type -AssemblyName System.Windows.Forms +$options = [System.Windows.Forms.MessageBoxOptions]::ServiceNotification +$defbutton = [System.Windows.Forms.MessageBoxDefaultButton]::Button1 +$buttons = [System.Windows.Forms.MessageBoxButtons]::OK +$icon = [System.Windows.Forms.MessageBoxIcon]::Error +$title = "Backrest" +$message = '{{ .Summary }}' +[System.Windows.Forms.MessageBox]::Show($message, $title, $buttons, $icon, $defbutton, $options) +``` + +#### Warning Toast Notification +Show temporary warning notifications. + +**Event:** `CONDITION_SNAPSHOT_WARNING` + +```sh +Add-Type -AssemblyName System.Windows.Forms +$balloon = New-Object System.Windows.Forms.NotifyIcon +$balloon.Icon = [System.Drawing.SystemIcons]::Warning +$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning +$balloon.BalloonTipText = '{{ .Summary }}' +$balloon.BalloonTipTitle = "Backrest" +$balloon.Visible = $true +$balloon.ShowBalloonTip(5000) +Start-Sleep -Seconds(5) +$balloon.Visible = $false +$balloon.Icon.Dispose() +$balloon.Dispose() +``` + +#### Success Toast Notification +Display temporary success notifications. + +**Event:** `CONDITION_SNAPSHOT_SUCCESS` + +```sh +Add-Type -AssemblyName System.Windows.Forms +$balloon = New-Object System.Windows.Forms.NotifyIcon +$balloon.Icon = [System.Drawing.SystemIcons]::Information +$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Information +$balloon.BalloonTipText = '{{ .Summary }}' +$balloon.BalloonTipTitle = "Backrest" +$balloon.Visible = $true +$balloon.ShowBalloonTip(5000) +Start-Sleep -Seconds(5) +$balloon.Visible = $false +$balloon.Icon.Dispose() +$balloon.Dispose() +``` diff --git a/docs/content/3.cookbooks/2.reverse-proxy-examples.md b/docs/content/3.cookbooks/2.reverse-proxy-examples.md new file mode 100644 index 000000000..2e03ab848 --- /dev/null +++ b/docs/content/3.cookbooks/2.reverse-proxy-examples.md @@ -0,0 +1,59 @@ +# Reverse Proxy Examples + +## Introduction + +Reverse proxies like [Caddy](https://caddyserver.com/) and [Traefik](https://traefik.io/traefik/) can be configured to front and protect your Backrest endpoint. + +## Using Caddy +For this example, we'll be running Caddy alongside Backrest via docker-compose.yaml but you can adapt this config to your environment. + +Here is an example docker-compose.yaml: +``` +version: "3.2" +services: + backrest: + image: garethgeorge/backrest + container_name: backrest + hostname: + volumes: + - ./backrest/data:/data + - ./backrest/config:/config + - ./backrest/cache:/cache + - /MY-BACKUP-DATA:/userdata # mount your directories to backup somewhere in the filesystem + - /MY-REPOS:/repos # (optional) mount your restic repositories somewhere in the filesystem. + environment: + - BACKREST_DATA=/data # path for backrest data. restic binary and the database are placed here. + - BACKREST_CONFIG=/config/config.json # path for the backrest config file. + - XDG_CACHE_HOME=/cache # path for the restic cache which greatly improves performance. + restart: unless-stopped + depends_on: + - caddy + caddy: + image: caddy + container_name: caddy + ports: + - "443:443" + - "443:443/udp" + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile + restart: unless-stopped +``` + +Your Caddyfile should look like this: +``` +{ + https_port 443 +} + +backrest.example.com { + tls internal + reverse_proxy backrest:9898 +} +``` + +Some items to note: +- The `reverse_proxy` line in your Caddyfile **must** match your Backrest container's name! +- You can extend this with [acme_dns](https://github.com/caddy-dns/acmedns) to obtain certificates for your endpoint. +- `tls internal` means that Caddy will generate and utilize a self-signed certificate. +- You can create an [authentication portal](https://caddyserver.com/docs/json/apps/http/servers/routes/handle/auth_portal/) to allow login via Google, etc. +- You can opt to have Caddy listen to requests on port 80 (HTTP) but that's not recommended for security reasons. diff --git a/docs/content/3.cookbooks/_dir.yml b/docs/content/3.cookbooks/_dir.yml new file mode 100644 index 000000000..31e3f2fa6 --- /dev/null +++ b/docs/content/3.cookbooks/_dir.yml @@ -0,0 +1,2 @@ +title: Cookbooks +icon: material-symbols:stockpot diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts new file mode 100755 index 000000000..b8c57acda --- /dev/null +++ b/docs/nuxt.config.ts @@ -0,0 +1,11 @@ +export default defineNuxtConfig({ + extends: ["@nuxt-themes/docus"], + devtools: { enabled: true }, + ssr: true, + + app: { + baseURL: "/backrest/", + }, + + compatibilityDate: "2025-02-19", +}); \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..e3433d9d5 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,16754 @@ +{ + "name": "backrest-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backrest-docs", + "version": "0.1.0", + "devDependencies": { + "@nuxt-themes/docus": "^1.14.8", + "@nuxt/devtools": "^1.3.1", + "@nuxt/eslint-config": "^0.3.13", + "@nuxtjs/plausible": "^1.0.0", + "@types/node": "^20.12.12", + "eslint": "^8.57.0", + "nuxt": "^3.11.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz", + "integrity": "sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-decorators": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz", + "integrity": "sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz", + "integrity": "sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-syntax-typescript": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/standalone": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.25.8.tgz", + "integrity": "sha512-UvRanvLCGPRscJ5Rw9o6vUBS5P+E+gkhl6eaokrIN+WM1kUkmj254VZhyihFdDZVDlI3cPcZoakbJJw24QPISw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@barbapapazes/plausible-tracker": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@barbapapazes/plausible-tracker/-/plausible-tracker-0.5.3.tgz", + "integrity": "sha512-b46xGOV7tUZA8yGzJDVh60rMANsq2RQf92+SW0Wjv7xbKaHVToKNzSIBfcRkRHouDJoljnvcPM26MfKaiDwGcw==", + "dev": true + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/kv-asset-handler/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", + "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@iconify/vue": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.1.2.tgz", + "integrity": "sha512-CQnYqLiQD5LOAaXhBrmj1mdL2/NCJvwcC4jtW2Z8ukhThiFkLDkutarTOV2trfc9EXqUqRs0KqXOL9pZ/IyysA==", + "dev": true, + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "vue": ">=3" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@netlify/functions": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.2.tgz", + "integrity": "sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==", + "dev": true, + "dependencies": { + "@netlify/serverless-functions-api": "1.26.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@netlify/node-cookies": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@netlify/node-cookies/-/node-cookies-0.1.0.tgz", + "integrity": "sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==", + "dev": true, + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, + "node_modules/@netlify/serverless-functions-api": { + "version": "1.26.1", + "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.26.1.tgz", + "integrity": "sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==", + "dev": true, + "dependencies": { + "@netlify/node-cookies": "^0.1.0", + "urlpattern-polyfill": "8.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt-themes/docus": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@nuxt-themes/docus/-/docus-1.15.0.tgz", + "integrity": "sha512-V2kJ5ecGUxXcEovXeQkJBPYfQwjmjaxB5fnl2XaQV+S2Epcn+vhPWShSlL6/WXzLPiAkQFdwbBj9xedTvXgjkw==", + "dev": true, + "dependencies": { + "@nuxt-themes/elements": "^0.9.5", + "@nuxt-themes/tokens": "^1.9.1", + "@nuxt-themes/typography": "^0.11.0", + "@nuxt/content": "^2.8.5", + "@nuxthq/studio": "^1.0.0", + "@vueuse/integrations": "^10.4.1", + "@vueuse/nuxt": "^10.4.1", + "focus-trap": "^7.5.3", + "fuse.js": "^6.6.2" + } + }, + "node_modules/@nuxt-themes/elements": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@nuxt-themes/elements/-/elements-0.9.5.tgz", + "integrity": "sha512-uAA5AiIaT1SxCBjNIURJyCDPNR27+8J+t3AWuzWyhbNPr3L1inEcETZ3RVNzFdQE6mx7MGAMwFBqxPkOUhZQuA==", + "dev": true, + "dependencies": { + "@nuxt-themes/tokens": "^1.9.1", + "@vueuse/core": "^9.13.0" + } + }, + "node_modules/@nuxt-themes/tokens": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@nuxt-themes/tokens/-/tokens-1.9.1.tgz", + "integrity": "sha512-5C28kfRvKnTX8Tux+xwyaf+2pxKgQ53dC9l6C33sZwRRyfUJulGDZCFjKbuNq4iqVwdGvkFSQBYBYjFAv6t75g==", + "dev": true, + "dependencies": { + "@nuxtjs/color-mode": "^3.2.0", + "@vueuse/core": "^9.13.0", + "pinceau": "^0.18.8" + } + }, + "node_modules/@nuxt-themes/typography": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@nuxt-themes/typography/-/typography-0.11.0.tgz", + "integrity": "sha512-TqyvD7sDWnqGmL00VtuI7JdmNTPL5/g957HCAWNzcNp+S20uJjW/FXSdkM76d4JSVDHvBqw7Wer3RsqVhqvA4w==", + "dev": true, + "dependencies": { + "@nuxtjs/color-mode": "^3.2.0", + "nuxt-config-schema": "^0.4.5", + "nuxt-icon": "^0.3.3", + "pinceau": "^0.18.8", + "ufo": "^1.1.1" + } + }, + "node_modules/@nuxt/content": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@nuxt/content/-/content-2.13.4.tgz", + "integrity": "sha512-NBaHL/SNYUK7+RLgOngSFmKqEPYc0dYdnwVFsxIdrOZUoUbD8ERJJDaoRwwtyYCMOgUeFA/zxAkuADytp+DKiQ==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.2", + "@nuxtjs/mdc": "^0.9.2", + "@vueuse/core": "^11.1.0", + "@vueuse/head": "^2.0.0", + "@vueuse/nuxt": "^11.1.0", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "json5": "^2.2.3", + "knitwork": "^1.1.0", + "listhen": "^1.9.0", + "mdast-util-to-string": "^4.0.0", + "mdurl": "^2.0.0", + "micromark": "^4.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-types": "^2.0.0", + "minisearch": "^7.1.0", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "shiki": "^1.22.0", + "slugify": "^1.6.6", + "socket.io-client": "^4.8.0", + "ufo": "^1.5.4", + "unist-util-stringify-position": "^4.0.0", + "unstorage": "^1.12.0", + "ws": "^8.18.0" + } + }, + "node_modules/@nuxt/content/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@nuxt/content/node_modules/@vueuse/core": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", + "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.1.0", + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/metadata": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", + "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/nuxt": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-11.1.0.tgz", + "integrity": "sha512-ZPYigcqgPPe9vk9nBHLF8p0zshX8qvWV/ox1Y4GdV4k2flPiw7+2THNTpU2NZDBXSOXlhB2sao+paGCsvJm/Qw==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.2", + "@vueuse/core": "11.1.0", + "@vueuse/metadata": "11.1.0", + "local-pkg": "^0.5.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "nuxt": "^3.0.0" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@nuxt/devalue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", + "integrity": "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==", + "dev": true + }, + "node_modules/@nuxt/devtools": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.6.0.tgz", + "integrity": "sha512-xNorMapzpM8HaW7NnAsEEO38OrmrYBzGvkkqfBU5nNh5XEymmIfCbQc7IA/GIOH9pXOV4gRutCjHCWXHYbOl3A==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@nuxt/devtools-kit": "1.6.0", + "@nuxt/devtools-wizard": "1.6.0", + "@nuxt/kit": "^3.13.2", + "@vue/devtools-core": "7.4.4", + "@vue/devtools-kit": "7.4.4", + "birpc": "^0.2.17", + "consola": "^3.2.3", + "cronstrue": "^2.50.0", + "destr": "^2.0.3", + "error-stack-parser-es": "^0.1.5", + "execa": "^7.2.0", + "fast-npm-meta": "^0.2.2", + "flatted": "^3.3.1", + "get-port-please": "^3.1.2", + "hookable": "^5.5.3", + "image-meta": "^0.2.1", + "is-installed-globally": "^1.0.0", + "launch-editor": "^2.9.1", + "local-pkg": "^0.5.0", + "magicast": "^0.3.5", + "nypm": "^0.3.11", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2", + "scule": "^1.3.0", + "semver": "^7.6.3", + "simple-git": "^3.27.0", + "sirv": "^2.0.4", + "tinyglobby": "^0.2.6", + "unimport": "^3.12.0", + "vite-plugin-inspect": "^0.8.7", + "vite-plugin-vue-inspector": "5.1.3", + "which": "^3.0.1", + "ws": "^8.18.0" + }, + "bin": { + "devtools": "cli.mjs" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/@nuxt/devtools-kit": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.6.0.tgz", + "integrity": "sha512-kJ8mVKwTSN3tdEVNy7mxKCiQk9wsG5t3oOrRMWk6IEbTSov+5sOULqQSM/+OWxWsEDmDfA7QlS5sM3Ti9uMRqQ==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.2", + "@nuxt/schema": "^3.13.2", + "execa": "^7.2.0" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/@nuxt/devtools-wizard": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.6.0.tgz", + "integrity": "sha512-n+mzz5NwnKZim0tq1oBi+x1nNXb21fp7QeBl7bYKyDT1eJ0XCxFkVTr/kB/ddkkLYZ+o8TykpeNPa74cN+xAyQ==", + "dev": true, + "dependencies": { + "consola": "^3.2.3", + "diff": "^7.0.0", + "execa": "^7.2.0", + "global-directory": "^4.0.1", + "magicast": "^0.3.5", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "prompts": "^2.4.2", + "rc9": "^2.1.2", + "semver": "^7.6.3" + }, + "bin": { + "devtools-wizard": "cli.mjs" + } + }, + "node_modules/@nuxt/eslint-config": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@nuxt/eslint-config/-/eslint-config-0.3.13.tgz", + "integrity": "sha512-xnMkcrz9vFjtIuKsfOPhNOKFVD51JZClj/16raciHVOK9eiqZuQjbxaf60b7ffk7cmD1EDhlQhbSxaLAJm/QYg==", + "dev": true, + "dependencies": { + "@eslint/js": "^9.2.0", + "@nuxt/eslint-plugin": "0.3.13", + "@rushstack/eslint-patch": "^1.10.3", + "@stylistic/eslint-plugin": "^2.1.0", + "@typescript-eslint/eslint-plugin": "^7.9.0", + "@typescript-eslint/parser": "^7.9.0", + "eslint-config-flat-gitignore": "^0.1.5", + "eslint-flat-config-utils": "^0.2.5", + "eslint-plugin-import-x": "^0.5.0", + "eslint-plugin-jsdoc": "^48.2.5", + "eslint-plugin-regexp": "^2.5.0", + "eslint-plugin-unicorn": "^53.0.0", + "eslint-plugin-vue": "^9.26.0", + "globals": "^15.2.0", + "pathe": "^1.1.2", + "tslib": "^2.6.2", + "vue-eslint-parser": "^9.4.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@nuxt/eslint-plugin": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@nuxt/eslint-plugin/-/eslint-plugin-0.3.13.tgz", + "integrity": "sha512-8LW9QJgVSARgO7QZmRy6vmWjDdHiAy/GNN3zKFPBetQxj5ECXsK0Ggfn8RiSi9rgqJSQjXDvMMHFpHiDETXgSQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "^7.9.0", + "@typescript-eslint/utils": "^7.9.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@nuxt/kit": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.13.2.tgz", + "integrity": "sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==", + "dev": true, + "dependencies": { + "@nuxt/schema": "3.13.2", + "c12": "^1.11.2", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "globby": "^14.0.2", + "hash-sum": "^2.0.0", + "ignore": "^5.3.2", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "ufo": "^1.5.4", + "unctx": "^2.3.1", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/schema": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.13.2.tgz", + "integrity": "sha512-CCZgpm+MkqtOMDEgF9SWgGPBXlQ01hV/6+2reDEpJuqFPGzV8HYKPBcIFvn7/z5ahtgutHLzjP71Na+hYcqSpw==", + "dev": true, + "dependencies": { + "compatx": "^0.1.8", + "consola": "^3.2.3", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/telemetry": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.6.0.tgz", + "integrity": "sha512-h4YJ1d32cU7tDKjjhjtIIEck4WF/w3DTQBT348E9Pz85YLttnLqktLM0Ez9Xc2LzCeUgBDQv1el7Ob/zT3KUqg==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.1", + "ci-info": "^4.0.0", + "consola": "^3.2.3", + "create-require": "^1.1.1", + "defu": "^6.1.4", + "destr": "^2.0.3", + "dotenv": "^16.4.5", + "git-url-parse": "^15.0.0", + "is-docker": "^3.0.0", + "jiti": "^1.21.6", + "mri": "^1.2.0", + "nanoid": "^5.0.7", + "ofetch": "^1.3.4", + "package-manager-detector": "^0.2.0", + "parse-git-config": "^3.0.0", + "pathe": "^1.1.2", + "rc9": "^2.1.2", + "std-env": "^3.7.0" + }, + "bin": { + "nuxt-telemetry": "bin/nuxt-telemetry.mjs" + } + }, + "node_modules/@nuxt/telemetry/node_modules/git-url-parse": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-15.0.0.tgz", + "integrity": "sha512-5reeBufLi+i4QD3ZFftcJs9jC26aULFLBU23FeKM/b1rI0K6ofIeAblmDVO7Ht22zTDE9+CkJ3ZVb0CgJmz3UQ==", + "dev": true, + "dependencies": { + "git-up": "^7.0.0" + } + }, + "node_modules/@nuxt/telemetry/node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@nuxt/vite-builder": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.13.2.tgz", + "integrity": "sha512-3dzc3YH3UeTmzGtCevW1jTq0Q8/cm+yXqo/VS/EFM3aIO/tuNPS88is8ZF2YeBButFnLFllq/QenziPbq0YD6Q==", + "dev": true, + "dependencies": { + "@nuxt/kit": "3.13.2", + "@rollup/plugin-replace": "^5.0.7", + "@vitejs/plugin-vue": "^5.1.3", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "autoprefixer": "^10.4.20", + "clear": "^0.1.0", + "consola": "^3.2.3", + "cssnano": "^7.0.6", + "defu": "^6.1.4", + "esbuild": "^0.23.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "externality": "^1.0.2", + "get-port-please": "^3.1.2", + "h3": "^1.12.0", + "knitwork": "^1.1.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "postcss": "^8.4.47", + "rollup-plugin-visualizer": "^5.12.0", + "std-env": "^3.7.0", + "strip-literal": "^2.1.0", + "ufo": "^1.5.4", + "unenv": "^1.10.0", + "unplugin": "^1.14.1", + "vite": "^5.4.5", + "vite-node": "^2.1.1", + "vite-plugin-checker": "^0.8.0", + "vue-bundle-renderer": "^2.1.0" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, + "node_modules/@nuxt/vite-builder/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/vite-builder/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@nuxthq/studio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nuxthq/studio/-/studio-1.1.2.tgz", + "integrity": "sha512-YVEiIuU+5cLZ0qdLsRAYuFE395XoYf87UTR5xwxxpw9++uhlyLiQyO7JIXTTWIOdEiMHt8frrrLJBBPd5tHAeQ==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.11.2", + "defu": "^6.1.4", + "git-url-parse": "^14.0.0", + "nuxt-component-meta": "^0.6.4", + "parse-git-config": "^3.0.0", + "pkg-types": "^1.1.1", + "socket.io-client": "^4.7.5", + "ufo": "^1.5.3", + "untyped": "^1.4.2" + } + }, + "node_modules/@nuxtjs/color-mode": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.5.1.tgz", + "integrity": "sha512-GRHF3WUwX6fXIiRVlngNq1nVDwrVuP6dWX1DRmox3QolzX0eH1oJEcFr/lAm1nkT71JVGb8mszho9w+yHJbePw==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.1", + "changelogen": "^0.5.5", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "semver": "^7.6.3" + } + }, + "node_modules/@nuxtjs/mdc": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.9.2.tgz", + "integrity": "sha512-dozIPTPjEYu8jChHNCICZP3mN0sFC6l3aLxTkgv/DAr1EI8jqqqoSZKevzuiHUWGNTguS70+fLcztCwrzWdoYA==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.13.2", + "@shikijs/transformers": "^1.22.0", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@vue/compiler-core": "^3.5.12", + "consola": "^3.2.3", + "debug": "^4.3.7", + "defu": "^6.1.4", + "destr": "^2.0.3", + "detab": "^3.0.2", + "github-slugger": "^2.0.0", + "hast-util-to-string": "^3.0.1", + "mdast-util-to-hast": "^13.2.0", + "micromark-util-sanitize-uri": "^2.0.0", + "ohash": "^1.1.4", + "parse5": "^7.2.0", + "pathe": "^1.1.2", + "property-information": "^6.5.0", + "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "rehype-sort-attribute-values": "^5.0.1", + "rehype-sort-attributes": "^5.0.1", + "remark-emoji": "^5.0.1", + "remark-gfm": "^4.0.0", + "remark-mdc": "^3.2.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "scule": "^1.3.0", + "shiki": "^1.22.0", + "ufo": "^1.5.4", + "unified": "^11.0.5", + "unist-builder": "^4.0.0", + "unist-util-visit": "^5.0.0", + "unwasm": "^0.3.9" + } + }, + "node_modules/@nuxtjs/plausible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nuxtjs/plausible/-/plausible-1.0.3.tgz", + "integrity": "sha512-jf6W9+Q/VhfHk/jal1gp0OpYU2qwq7eOV4evNKvHWKVM0Qbps+LbKSxNcewgqN91tVx9sv4RbnOEJ8Uq0/FRkg==", + "dev": true, + "dependencies": { + "@barbapapazes/plausible-tracker": "^0.5.3", + "@nuxt/kit": "^3.13.2", + "defu": "^6.1.4" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz", + "integrity": "sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==", + "bundleDependencies": [ + "napi-wasm" + ], + "dev": true, + "dependencies": { + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "napi-wasm": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, + "node_modules/@rollup/plugin-alias": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", + "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@shikijs/core": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", + "dev": true, + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", + "dev": true, + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.22.0.tgz", + "integrity": "sha512-k7iMOYuGQA62KwAuJOQBgH2IQb5vP8uiB3lMvAMGUgAMMurePOx3Z7oNqJdcpxqZP6I9cc7nc4DNqSKduCxmdg==", + "dev": true, + "dependencies": { + "shiki": "1.22.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.9.0.tgz", + "integrity": "sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^8.8.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz", + "integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@unhead/dom": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.10.tgz", + "integrity": "sha512-nL1mdRzYVATZIYauK15zOI2YyM3YxCLfhbTqljEjDFJeiJUzTTi+a//5FHiUk84ewSucFnrwHNey/pEXFlyY1A==", + "dev": true, + "dependencies": { + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/schema": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.10.tgz", + "integrity": "sha512-lXh7cm5XtFaw3gc+ZVXTSfIHXiBpAywbjtEiOsz5TR4GxOjj2rtfOAl4C3Difk1yupP6L2otYmOZdn/i8EXSJg==", + "dev": true, + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/shared": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.10.tgz", + "integrity": "sha512-YQgZcOyo1id7drUeDPGn0R83pirvIcV+Car3/m7ZfCLL1Syab6uXmRckVRd69yVbUL4eirIm9IzzmvzM/OuGuw==", + "dev": true, + "dependencies": { + "@unhead/schema": "1.11.10" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/ssr": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.10.tgz", + "integrity": "sha512-tj5zeJtCbSktNNqsdL+6h6OIY7dYO+2HSiC1VbofGYsoG7nDNXMypkrW/cTMqZVr5/gWhKaUgFQALjm28CflYg==", + "dev": true, + "dependencies": { + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/vue": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.10.tgz", + "integrity": "sha512-v6ddp4YEQCNILhYrx37Yt0GKRIFeTrb3VSmTbjh+URT+ua1mwgmNFTfl2ZldtTtri3tEkwSG1/5wLRq20ma70g==", + "dev": true, + "dependencies": { + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "unhead": "1.11.10" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, + "node_modules/@unocss/reset": { + "version": "0.50.8", + "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-0.50.8.tgz", + "integrity": "sha512-2WoM6O9VyuHDPAnvCXr7LBJQ8ZRHDnuQAFsL1dWXp561Iq2l9whdNtPuMcozLGJGUUrFfVBXIrHY4sfxxScgWg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vercel/nft": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz", + "integrity": "sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==", + "dev": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "@rollup/pluginutils": "^4.0.0", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.2", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.2", + "node-gyp-build": "^4.2.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/nft/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vercel/nft/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vercel/nft/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vercel/nft/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vercel/nft/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vercel/nft/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz", + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7", + "@vue/babel-plugin-jsx": "^1.2.2" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.4.1.tgz", + "integrity": "sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.4.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.4.1.tgz", + "integrity": "sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==", + "dev": true, + "dependencies": { + "muggle-string": "^0.2.2" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript/node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/@volar/vue-language-core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.6.5.tgz", + "integrity": "sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.4.1", + "@volar/source-map": "1.4.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/compiler-sfc": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.2.2", + "vue-template-compiler": "^2.7.14" + } + }, + "node_modules/@vue-macros/common": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.15.0.tgz", + "integrity": "sha512-yg5VqW7+HRfJGimdKvFYzx8zorHUYo0hzPwuraoC1DWa7HHazbTMoVsHDvk3JHa1SGfSL87fRnzmlvgjEHhszA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.8", + "@rollup/pluginutils": "^5.1.2", + "@vue/compiler-sfc": "^3.5.12", + "ast-kit": "^1.3.0", + "local-pkg": "^0.5.0", + "magic-string-ast": "^0.6.2" + }, + "engines": { + "node": ">=16.14.0" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", + "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", + "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.6", + "@babel/types": "^7.25.6", + "@vue/babel-helper-vue-transform-on": "1.2.5", + "@vue/babel-plugin-resolve-type": "1.2.5", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", + "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/parser": "^7.25.6", + "@vue/compiler-sfc": "^3.5.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "dev": true + }, + "node_modules/@vue/devtools-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.4.4.tgz", + "integrity": "sha512-DLxgA3DfeADkRzhAfm3G2Rw/cWxub64SdP5b+s5dwL30+whOGj+QNhmyFpwZ8ZTrHDFRIPj0RqNzJ8IRR1pz7w==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.4.4", + "@vue/devtools-shared": "^7.4.4", + "mitt": "^3.0.1", + "nanoid": "^3.3.4", + "pathe": "^1.1.2", + "vite-hot-client": "^0.2.3" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.4.4.tgz", + "integrity": "sha512-awK/4NfsUG0nQ7qnTM37m7ZkEUMREyPh8taFCX+uQYps/MTFEum0AD05VeGDRMXwWvMmGIcWX9xp8ZiBddY0jw==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.4.4", + "birpc": "^0.2.17", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.5.2.tgz", + "integrity": "sha512-+zmcixnD6TAo+zwm30YuwZckhL9iIi4u+gFwbq9C8zpm3SMndTlEYZtNhAHUhOXB+bCkzyunxw80KQ/T0trF4w==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@vue/language-core/node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@vue/language-core/node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "vue": "3.5.12" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/head": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/head/-/head-2.0.0.tgz", + "integrity": "sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==", + "dev": true, + "dependencies": { + "@unhead/dom": "^1.7.0", + "@unhead/schema": "^1.7.0", + "@unhead/ssr": "^1.7.0", + "@unhead/vue": "^1.7.0" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, + "node_modules/@vueuse/integrations": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.1.tgz", + "integrity": "sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==", + "dev": true, + "dependencies": { + "@vueuse/core": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^4", + "drauu": "^0.3", + "focus-trap": "^7", + "fuse.js": "^6", + "idb-keyval": "^6", + "jwt-decode": "^3", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^6" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-10.11.1.tgz", + "integrity": "sha512-UiaYSIwOkmUVn8Gl1AqtLWYR12flO+8sEu9X0Y1fNjSR7EWy9jMuiCvOGqwtoeTsqfHrivl0d5HfMzr11GFnMA==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.12.1", + "@vueuse/core": "10.11.1", + "@vueuse/metadata": "10.11.1", + "local-pkg": "^0.5.0", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "nuxt": "^3.0.0" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "dev": true, + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/ast-kit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.3.0.tgz", + "integrity": "sha512-ORycPY6qYSrAGMnSk1tlqy/Y0rFGk/WIYP/H6io0A+jXK2Jp3Il7h8vjfwaLvZUwanjiLwBeE5h3A9M+eQqeNw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.8", + "pathe": "^1.1.2" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", + "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "ast-kit": "^1.0.1" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", + "dev": true, + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/changelogen": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/changelogen/-/changelogen-0.5.7.tgz", + "integrity": "sha512-cTZXBcJMl3pudE40WENOakXkcVtrbBpbkmSkM20NdRiUqa4+VYRdXdEsgQ0BNQ6JBE2YymTNWtPKVF7UCTN5+g==", + "dev": true, + "dependencies": { + "c12": "^1.11.2", + "colorette": "^2.0.20", + "consola": "^3.2.3", + "convert-gitmoji": "^0.1.5", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "open": "^10.1.0", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "std-env": "^3.7.0", + "yaml": "^2.5.1" + }, + "bin": { + "changelogen": "dist/cli.mjs" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chroma-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", + "dev": true + }, + "node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/clear": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", + "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/clipboardy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", + "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", + "dev": true, + "dependencies": { + "execa": "^8.0.1", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/clipboardy/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/convert-gitmoji": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/convert-gitmoji/-/convert-gitmoji-0.1.5.tgz", + "integrity": "sha512-4wqOafJdk2tqZC++cjcbGcaJ13BZ3kwldf06PTiAQRAB76Z1KJwZNL1SaRZMi2w1FM9RYTgZ6QErS8NUl/GBmQ==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/croner": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/croner/-/croner-8.1.2.tgz", + "integrity": "sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog==", + "dev": true, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/cronstrue": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", + "integrity": "sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==", + "dev": true, + "bin": { + "cronstrue": "bin/cli.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crossws": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz", + "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==", + "dev": true, + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", + "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^7.0.6", + "lilconfig": "^3.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", + "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.0", + "postcss-calc": "^10.0.2", + "postcss-colormin": "^7.0.2", + "postcss-convert-values": "^7.0.4", + "postcss-discard-comments": "^7.0.3", + "postcss-discard-duplicates": "^7.0.1", + "postcss-discard-empty": "^7.0.0", + "postcss-discard-overridden": "^7.0.0", + "postcss-merge-longhand": "^7.0.4", + "postcss-merge-rules": "^7.0.4", + "postcss-minify-font-values": "^7.0.0", + "postcss-minify-gradients": "^7.0.0", + "postcss-minify-params": "^7.0.2", + "postcss-minify-selectors": "^7.0.4", + "postcss-normalize-charset": "^7.0.0", + "postcss-normalize-display-values": "^7.0.0", + "postcss-normalize-positions": "^7.0.0", + "postcss-normalize-repeat-style": "^7.0.0", + "postcss-normalize-string": "^7.0.0", + "postcss-normalize-timing-functions": "^7.0.0", + "postcss-normalize-unicode": "^7.0.2", + "postcss-normalize-url": "^7.0.0", + "postcss-normalize-whitespace": "^7.0.0", + "postcss-ordered-values": "^7.0.1", + "postcss-reduce-initial": "^7.0.2", + "postcss-reduce-transforms": "^7.0.0", + "postcss-svgo": "^7.0.1", + "postcss-unique-selectors": "^7.0.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", + "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/db0": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/db0/-/db0-0.1.4.tgz", + "integrity": "sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==", + "dev": true, + "peerDependencies": { + "@libsql/client": "^0.5.2", + "better-sqlite3": "^9.4.3", + "drizzle-orm": "^0.29.4" + }, + "peerDependenciesMeta": { + "@libsql/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detab": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detab/-/detab-3.0.2.tgz", + "integrity": "sha512-7Bp16Bk8sk0Y6gdXiCtnpGbghn8atnTJdd/82aWvS5ESnlcNvgUc10U2NYS0PAiDSGjWiI8qs/Cv1b2uSGdQ8w==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz", + "integrity": "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==", + "dev": true, + "dependencies": { + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-flat-gitignore": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-0.1.8.tgz", + "integrity": "sha512-OEUbS2wzzYtUfshjOqzFo4Bl4lHykXUdM08TCnYNl7ki+niW4Q1R0j0FDFDr0vjVsI5ZFOz5LvluxOP+Ew+dYw==", + "dev": true, + "dependencies": { + "find-up-simple": "^1.0.0", + "parse-gitignore": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/eslint-flat-config-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/eslint-flat-config-utils/-/eslint-flat-config-utils-0.2.5.tgz", + "integrity": "sha512-iO+yLZtC/LKgACerkpvsZ6NoRVB2sxT04mOpnNcEM1aTwKy+6TsT46PUvrML4y2uVBS6I67hRCd2JiKAPaL/Uw==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import-x": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-0.5.3.tgz", + "integrity": "sha512-hJ/wkMcsLQXAZL3+txXIDpbW5cqwdm1rLTqV4VRY03aIbzE3zWE7rPZKW6Gzf7xyl1u3V1iYC6tOG77d9NF4GQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^7.4.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "stable-hash": "^0.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^8.56.0 || ^9.0.0-0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "48.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.11.0.tgz", + "integrity": "sha512-d12JHJDPNo7IFwTOAItCeJY1hcqoIxE0lHA8infQByLilQ9xkqrRa6laWCnsuCrf+8rUnvxXY1XuTbibRBNylA==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.46.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.5", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-regexp": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.6.0.tgz", + "integrity": "sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.9.1", + "comment-parser": "^1.4.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "refa": "^0.12.1", + "regexp-ast-analysis": "^0.7.1", + "scslre": "^0.3.0" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "eslint": ">=8.44.0" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "53.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz", + "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^3.0.2", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.1.tgz", + "integrity": "sha512-MH/MbVae4HV/tM8gKAVWMPJbYgW04CK7SuzYRrlNERpxbO0P3+Zdsa2oAcFBW6xNu7W6lIkGOsFAMCRTYmrlWQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/externality": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", + "integrity": "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw==", + "dev": true, + "dependencies": { + "enhanced-resolve": "^5.14.1", + "mlly": "^1.3.0", + "pathe": "^1.1.1", + "ufo": "^1.1.2" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-npm-meta": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.2.2.tgz", + "integrity": "sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, + "bin": { + "flat": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "dev": true + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-config-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", + "integrity": "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dev": true, + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^8.1.0" + } + }, + "node_modules/git-url-parse": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.1.0.tgz", + "integrity": "sha512-8xg65dTxGHST3+zGpycMMFZcoTzAdZ2dOtu4vmgIfkTFnVHBxHMzBC2L1k8To7EmrSiHesT8JgPLT91VKw1B5g==", + "dev": true, + "dependencies": { + "git-up": "^7.0.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz", + "integrity": "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/h3": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz", + "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==", + "dev": true, + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": ">=0.2.0 <0.4.0", + "defu": "^6.1.4", + "destr": "^2.0.3", + "iron-webcrypto": "^1.2.1", + "ohash": "^1.1.4", + "radix3": "^1.1.2", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unenv": "^1.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-shutdown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", + "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/httpxy": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.1.5.tgz", + "integrity": "sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==", + "dev": true + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-meta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", + "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/impound": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/impound/-/impound-0.1.0.tgz", + "integrity": "sha512-F9nJgOsDc3tysjN74edE0vGPEQrU7DAje6g5nNAL5Jc9Tv4JW3mH7XMGne+EaadTniDXLeUrVR21opkNfWO1zQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "unenv": "^1.10.0", + "unplugin": "^1.12.2" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dev": true, + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "dev": true, + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.1.0.tgz", + "integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==", + "dev": true + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/listhen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", + "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", + "dev": true, + "dependencies": { + "@parcel/watcher": "^2.4.1", + "@parcel/watcher-wasm": "^2.4.1", + "citty": "^0.1.6", + "clipboardy": "^4.0.0", + "consola": "^3.2.3", + "crossws": ">=0.2.0 <0.4.0", + "defu": "^6.1.4", + "get-port-please": "^3.1.2", + "h3": "^1.12.0", + "http-shutdown": "^1.2.2", + "jiti": "^2.1.2", + "mlly": "^1.7.1", + "node-forge": "^1.3.1", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "untun": "^0.1.3", + "uqr": "^0.1.2" + }, + "bin": { + "listen": "bin/listhen.mjs", + "listhen": "bin/listhen.mjs" + } + }, + "node_modules/listhen/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magic-string-ast": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.2.tgz", + "integrity": "sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.10" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minisearch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", + "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdist": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mkdist/-/mkdist-1.6.0.tgz", + "integrity": "sha512-nD7J/mx33Lwm4Q4qoPgRBVA9JQNKgyE7fLo5vdPWVDdjz96pXglGERp/fRnGPCTB37Kykfxs5bDdXa9BWOT9nw==", + "dev": true, + "dependencies": { + "autoprefixer": "^10.4.20", + "citty": "^0.1.6", + "cssnano": "^7.0.6", + "defu": "^6.1.4", + "esbuild": "^0.24.0", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "postcss": "^8.4.45", + "postcss-nested": "^6.2.0", + "semver": "^7.6.3", + "tinyglobby": "^0.2.9" + }, + "bin": { + "mkdist": "dist/cli.cjs" + }, + "peerDependencies": { + "sass": "^1.78.0", + "typescript": ">=5.5.4", + "vue-tsc": "^1.8.27 || ^2.0.21" + }, + "peerDependenciesMeta": { + "sass": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/mkdist/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/mkdist/node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz", + "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanotar": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/nanotar/-/nanotar-0.1.1.tgz", + "integrity": "sha512-AiJsGsSF3O0havL1BydvI4+wR76sKT+okKRwWIaK96cZUnXqH0uNBOsHlbwZq3+m2BR1VKqHDVudl3gO4mYjpQ==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nitropack": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.9.7.tgz", + "integrity": "sha512-aKXvtNrWkOCMsQbsk4A0qQdBjrJ1ZcvwlTQevI/LAgLWLYc5L7Q/YiYxGLal4ITyNSlzir1Cm1D2ZxnYhmpMEw==", + "dev": true, + "dependencies": { + "@cloudflare/kv-asset-handler": "^0.3.4", + "@netlify/functions": "^2.8.0", + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.8", + "@rollup/plugin-inject": "^5.0.5", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/pluginutils": "^5.1.0", + "@types/http-proxy": "^1.17.14", + "@vercel/nft": "^0.26.5", + "archiver": "^7.0.1", + "c12": "^1.11.1", + "chalk": "^5.3.0", + "chokidar": "^3.6.0", + "citty": "^0.1.6", + "consola": "^3.2.3", + "cookie-es": "^1.1.0", + "croner": "^8.0.2", + "crossws": "^0.2.4", + "db0": "^0.1.4", + "defu": "^6.1.4", + "destr": "^2.0.3", + "dot-prop": "^8.0.2", + "esbuild": "^0.20.2", + "escape-string-regexp": "^5.0.0", + "etag": "^1.8.1", + "fs-extra": "^11.2.0", + "globby": "^14.0.1", + "gzip-size": "^7.0.0", + "h3": "^1.12.0", + "hookable": "^5.5.3", + "httpxy": "^0.1.5", + "ioredis": "^5.4.1", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "listhen": "^1.7.2", + "magic-string": "^0.30.10", + "mime": "^4.0.3", + "mlly": "^1.7.1", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "ohash": "^1.1.3", + "openapi-typescript": "^6.7.6", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.1", + "pretty-bytes": "^6.1.1", + "radix3": "^1.1.2", + "rollup": "^4.18.0", + "rollup-plugin-visualizer": "^5.12.0", + "scule": "^1.3.0", + "semver": "^7.6.2", + "serve-placeholder": "^2.0.2", + "serve-static": "^1.15.0", + "std-env": "^3.7.0", + "ufo": "^1.5.3", + "uncrypto": "^0.1.3", + "unctx": "^2.3.1", + "unenv": "^1.9.0", + "unimport": "^3.7.2", + "unstorage": "^1.10.2", + "unwasm": "^0.3.9" + }, + "bin": { + "nitro": "dist/cli/index.mjs", + "nitropack": "dist/cli/index.mjs" + }, + "engines": { + "node": "^16.11.0 || >=17.0.0" + }, + "peerDependencies": { + "xml2js": "^0.6.2" + }, + "peerDependenciesMeta": { + "xml2js": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/nitropack/node_modules/crossws": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.2.4.tgz", + "integrity": "sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==", + "dev": true, + "peerDependencies": { + "uWebSockets.js": "*" + }, + "peerDependenciesMeta": { + "uWebSockets.js": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/nitropack/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nuxi": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.14.0.tgz", + "integrity": "sha512-MhG4QR6D95jQxhnwKfdKXulZ8Yqy1nbpwbotbxY5IcabOzpEeTB8hYn2BFkmYdMUB0no81qpv2ldZmVCT9UsnQ==", + "dev": true, + "bin": { + "nuxi": "bin/nuxi.mjs", + "nuxi-ng": "bin/nuxi.mjs", + "nuxt": "bin/nuxi.mjs", + "nuxt-cli": "bin/nuxi.mjs" + }, + "engines": { + "node": "^16.10.0 || >=18.0.0" + } + }, + "node_modules/nuxt": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.13.2.tgz", + "integrity": "sha512-Bjc2qRsipfBhjXsBEJCN+EUAukhdgFv/KoIR5HFB2hZOYRSqXBod3oWQs78k3ja1nlIhAEdBG533898KJxUtJw==", + "dev": true, + "dependencies": { + "@nuxt/devalue": "^2.0.2", + "@nuxt/devtools": "^1.4.2", + "@nuxt/kit": "3.13.2", + "@nuxt/schema": "3.13.2", + "@nuxt/telemetry": "^2.6.0", + "@nuxt/vite-builder": "3.13.2", + "@unhead/dom": "^1.11.5", + "@unhead/shared": "^1.11.5", + "@unhead/ssr": "^1.11.5", + "@unhead/vue": "^1.11.5", + "@vue/shared": "^3.5.5", + "acorn": "8.12.1", + "c12": "^1.11.2", + "chokidar": "^3.6.0", + "compatx": "^0.1.8", + "consola": "^3.2.3", + "cookie-es": "^1.2.2", + "defu": "^6.1.4", + "destr": "^2.0.3", + "devalue": "^5.0.0", + "errx": "^0.1.0", + "esbuild": "^0.23.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "globby": "^14.0.2", + "h3": "^1.12.0", + "hookable": "^5.5.3", + "ignore": "^5.3.2", + "impound": "^0.1.0", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "nanotar": "^0.1.1", + "nitropack": "^2.9.7", + "nuxi": "^3.13.2", + "nypm": "^0.3.11", + "ofetch": "^1.3.4", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "radix3": "^1.1.2", + "scule": "^1.3.0", + "semver": "^7.6.3", + "std-env": "^3.7.0", + "strip-literal": "^2.1.0", + "tinyglobby": "0.2.6", + "ufo": "^1.5.4", + "ultrahtml": "^1.5.3", + "uncrypto": "^0.1.3", + "unctx": "^2.3.1", + "unenv": "^1.10.0", + "unhead": "^1.11.5", + "unimport": "^3.12.0", + "unplugin": "^1.14.1", + "unplugin-vue-router": "^0.10.8", + "unstorage": "^1.12.0", + "untyped": "^1.4.2", + "vue": "^3.5.5", + "vue-bundle-renderer": "^2.1.0", + "vue-devtools-stub": "^0.1.0", + "vue-router": "^4.4.5" + }, + "bin": { + "nuxi": "bin/nuxt.mjs", + "nuxt": "bin/nuxt.mjs" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + }, + "peerDependencies": { + "@parcel/watcher": "^2.1.0", + "@types/node": "^14.18.0 || >=16.10.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/nuxt-component-meta": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nuxt-component-meta/-/nuxt-component-meta-0.6.6.tgz", + "integrity": "sha512-Y5/tuZuZOlD4GluAjcTU6JlhtEeg7/92VEfoV814t2uTuZK+b9RokJeGtrMotXuCJ4vuT1Is7M+pkPm+vY/tXA==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.9.1", + "citty": "^0.1.5", + "scule": "^1.1.1", + "typescript": "^5.3.3", + "vue-component-meta": "^1.8.27" + }, + "bin": { + "nuxt-component-meta": "bin/nuxt-component-meta.mjs" + } + }, + "node_modules/nuxt-config-schema": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/nuxt-config-schema/-/nuxt-config-schema-0.4.6.tgz", + "integrity": "sha512-kHLWJFynj5QrxVZ1MjY2xmDaTSN1BCMLGExA+hMMLoCb3wn9TJlDVqnE/nSdUJPMRkNn/NQ5WP9NLA9vlAXRUw==", + "dev": true, + "dependencies": { + "@nuxt/kit": "^3.4.2", + "defu": "^6.1.2", + "jiti": "^1.18.2", + "pathe": "^1.0.0", + "untyped": "^1.3.2" + } + }, + "node_modules/nuxt-icon": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/nuxt-icon/-/nuxt-icon-0.3.3.tgz", + "integrity": "sha512-KdhJAigBGTP8/YIFZ3orwetk40AgLq6VQ5HRYuDLmv5hiDptor9Ro+WIdZggHw7nciRxZvDdQkEwi9B5G/jrkQ==", + "dev": true, + "dependencies": { + "@iconify/vue": "^4.1.0", + "@nuxt/kit": "^3.3.1", + "nuxt-config-schema": "^0.4.5" + } + }, + "node_modules/nuxt/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nuxt/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nuxt/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/nuxt/node_modules/tinyglobby": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.6.tgz", + "integrity": "sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==", + "dev": true, + "dependencies": { + "fdir": "^6.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "dev": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/nypm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/nypm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "dev": true, + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.6.tgz", + "integrity": "sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "fast-glob": "^3.3.2", + "js-yaml": "^4.1.0", + "supports-color": "^9.4.0", + "undici": "^5.28.4", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/package-manager-detector": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", + "dev": true + }, + "node_modules/paneer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/paneer/-/paneer-0.1.0.tgz", + "integrity": "sha512-SZfJe/y9fbpeXZU+Kf7cSG2G7rnGP50hUYzCvcWyhp7hYzA3YXGthpkGfv6NSt0oo6QbcRyKwycg/6dpG5p8aw==", + "deprecated": "Please migrate to https://github.com/unjs/magicast", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@types/estree": "^1.0.0", + "recast": "^0.22.0" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true + }, + "node_modules/parse-git-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz", + "integrity": "sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==", + "dev": true, + "dependencies": { + "git-config-path": "^2.0.0", + "ini": "^1.3.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-git-config/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/parse-gitignore": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", + "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "dev": true, + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dev": true, + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dev": true, + "dependencies": { + "parse-path": "^7.0.0" + } + }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinceau": { + "version": "0.18.9", + "resolved": "https://registry.npmjs.org/pinceau/-/pinceau-0.18.9.tgz", + "integrity": "sha512-GJ+l8a5Y+7PP/diwuajJhd2QONTIFkk2YXjrVTh7QKC3sMQEphpLH6ZJfXSeeSonQ0/BnhrrMi9a5e14mmqXug==", + "dev": true, + "dependencies": { + "@unocss/reset": "^0.50.3", + "@volar/vue-language-core": "^1.2.0", + "acorn": "^8.8.2", + "chroma-js": "^2.4.2", + "consola": "^3.0.1", + "csstype": "^3.1.1", + "defu": "^6.1.2", + "magic-string": "^0.30.0", + "nanoid": "^4.0.1", + "ohash": "^1.0.0", + "paneer": "^0.1.0", + "pathe": "^1.1.0", + "postcss-custom-properties": "13.1.4", + "postcss-dark-theme-class": "0.7.3", + "postcss-nested": "^6.0.1", + "recast": "^0.22.0", + "scule": "^1.0.0", + "style-dictionary-esm": "^1.3.7", + "unbuild": "^1.1.2", + "unplugin": "^1.1.0" + } + }, + "node_modules/pinceau/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", + "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", + "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", + "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-properties": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.1.4.tgz", + "integrity": "sha512-iSAdaZrM3KMec8cOSzeTUNXPYDlhqsMJHpt62yrjwG6nAnMtRHPk5JdMzGosBJtqEahDolvD5LNbcq+EZ78o5g==", + "dev": true, + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.0", + "@csstools/css-parser-algorithms": "^2.0.0", + "@csstools/css-tokenizer": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dark-theme-class": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/postcss-dark-theme-class/-/postcss-dark-theme-class-0.7.3.tgz", + "integrity": "sha512-M9vtfh8ORzQsVdT9BWb+xpEDAzC7nHBn7wVc988/JkEVLPupKcUnV0jw7RZ8sSj0ovpqN1POf6PLdt19JCHfhQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", + "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", + "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", + "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", + "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", + "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.4" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", + "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.0", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", + "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", + "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "dev": true, + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", + "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", + "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", + "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", + "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", + "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", + "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", + "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", + "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", + "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", + "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", + "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", + "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", + "dev": true, + "dependencies": { + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", + "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", + "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", + "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.3.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", + "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recast": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", + "integrity": "sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==", + "dev": true, + "dependencies": { + "assert": "^2.0.0", + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true + }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sort-attribute-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-sort-attribute-values/-/rehype-sort-attribute-values-5.0.1.tgz", + "integrity": "sha512-lU3ABJO5frbUgV132YS6SL7EISf//irIm9KFMaeu5ixHfgWf6jhe+09Uf/Ef8pOYUJWKOaQJDRJGCXs6cNsdsQ==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sort-attributes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-sort-attributes/-/rehype-sort-attributes-5.0.1.tgz", + "integrity": "sha512-Bxo+AKUIELcnnAZwJDt5zUDDRpt4uzhfz9d0PVGhcxYWsbFj5Cv35xuWxu5r1LeYNFNhgGqsr9Q2QiIOM/Qctg==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz", + "integrity": "sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.4", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.3", + "unified": "^11.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/remark-mdc/-/remark-mdc-3.2.1.tgz", + "integrity": "sha512-MLNqQE7ryygOA3TtH4hKmIvmjFAqTMzCs2zrMzXs4MWJXYM2vbtdwR2NfgcN3vxIp5Pllgq3oLGuKgQSs8J19w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "@types/unist": "^3.0.2", + "flat": "^6.0.1", + "js-yaml": "^4.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "micromark": "^4.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.1.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.1", + "scule": "^1.3.0", + "stringify-entities": "^4.0.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-placeholder": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", + "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", + "dev": true, + "dependencies": { + "defu": "^6.1.4" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shiki": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", + "dev": true, + "dependencies": { + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-git": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", + "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true + }, + "node_modules/style-dictionary-esm": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/style-dictionary-esm/-/style-dictionary-esm-1.9.2.tgz", + "integrity": "sha512-MR+ppTqzkJJtXH6UyDJ0h4h4ekBCePA8A8xlYNuL0tLj2K+ngyuxoe0AvCHQ7sJVX8O5WK2z32ANSgIcF4mGxw==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "change-case": "^4.1.2", + "commander": "^11.1.0", + "consola": "^3.2.3", + "fast-glob": "^3.3.2", + "glob": "^10.3.10", + "jiti": "^1.21.0", + "json5": "^2.2.3", + "jsonc-parser": "^3.2.0", + "lodash.template": "^4.5.0", + "tinycolor2": "^1.6.0" + }, + "bin": { + "style-dictionary": "bin/style-dictionary.js" + } + }, + "node_modules/style-dictionary-esm/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/style-dictionary-esm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/style-dictionary-esm/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stylehacks": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", + "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true + }, + "node_modules/ultrahtml": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", + "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==", + "dev": true + }, + "node_modules/unbuild": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/unbuild/-/unbuild-1.2.1.tgz", + "integrity": "sha512-J4efk69Aye43tWcBPCsLK7TIRppGrEN4pAlDzRKo3HSE6MgTSTBxSEuE3ccx7ixc62JvGQ/CoFXYqqF2AHozow==", + "dev": true, + "dependencies": { + "@rollup/plugin-alias": "^5.0.0", + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/pluginutils": "^5.0.2", + "chalk": "^5.2.0", + "consola": "^3.0.2", + "defu": "^6.1.2", + "esbuild": "^0.17.16", + "globby": "^13.1.4", + "hookable": "^5.5.3", + "jiti": "^1.18.2", + "magic-string": "^0.30.0", + "mkdist": "^1.2.0", + "mlly": "^1.2.0", + "mri": "^1.2.0", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "pretty-bytes": "^6.1.0", + "rollup": "^3.20.2", + "rollup-plugin-dts": "^5.3.0", + "scule": "^1.0.0", + "typescript": "^5.0.4", + "untyped": "^1.3.2" + }, + "bin": { + "unbuild": "dist/cli.mjs" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@rollup/plugin-commonjs": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", + "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/unbuild/node_modules/@rollup/plugin-commonjs/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/unbuild/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/unbuild/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbuild/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unbuild/node_modules/rollup-plugin-dts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz", + "integrity": "sha512-gusMi+Z4gY/JaEQeXnB0RUdU82h1kF0WYzCWgVmV4p3hWXqelaKuCvcJawfeg+EKn2T1Ie+YWF2OiN1/L8bTVg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.22.5" + }, + "peerDependencies": { + "rollup": "^3.0", + "typescript": "^4.1 || ^5.0" + } + }, + "node_modules/unbuild/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "dev": true + }, + "node_modules/unctx": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.3.1.tgz", + "integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + } + }, + "node_modules/unctx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unenv": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", + "dev": true, + "dependencies": { + "consola": "^3.2.3", + "defu": "^6.1.4", + "mime": "^3.0.0", + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" + } + }, + "node_modules/unenv/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/unhead": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.10.tgz", + "integrity": "sha512-hypXrAI47wE3wIhkze0RMPGAWcoo45Q1+XzdqLD/OnTCzjFXQrpuE4zBy8JRexyrqp+Ud2+nFTUNf/mjfFSymw==", + "dev": true, + "dependencies": { + "@unhead/dom": "1.11.10", + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10", + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unimport": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.13.1.tgz", + "integrity": "sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.2", + "acorn": "^8.12.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.0", + "unplugin": "^1.14.1" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", + "dev": true, + "dependencies": { + "acorn": "^8.12.1", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-router": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.8.tgz", + "integrity": "sha512-xi+eLweYAqolIoTRSmumbi6Yx0z5M0PLvl+NFNVWHJgmE2ByJG1SZbrn+TqyuDtIyln20KKgq8tqmL7aLoiFjw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.4", + "@rollup/pluginutils": "^5.1.0", + "@vue-macros/common": "^1.12.2", + "ast-walker-scope": "^0.6.2", + "chokidar": "^3.6.0", + "fast-glob": "^3.3.2", + "json5": "^2.2.3", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "unplugin": "^1.12.2", + "yaml": "^2.5.0" + }, + "peerDependencies": { + "vue-router": "^4.4.0" + }, + "peerDependenciesMeta": { + "vue-router": { + "optional": true + } + } + }, + "node_modules/unstorage": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.12.0.tgz", + "integrity": "sha512-ARZYTXiC+e8z3lRM7/qY9oyaOkaozCeNd2xoz7sYK9fv7OLGhVsf+BZbmASqiK/HTZ7T6eAlnVq9JynZppyk3w==", + "dev": true, + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^3.6.0", + "destr": "^2.0.3", + "h3": "^1.12.0", + "listhen": "^1.7.2", + "lru-cache": "^10.4.3", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "ufo": "^1.5.4" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.7.0", + "@azure/cosmos": "^4.1.1", + "@azure/data-tables": "^13.2.2", + "@azure/identity": "^4.4.1", + "@azure/keyvault-secrets": "^4.8.0", + "@azure/storage-blob": "^12.24.0", + "@capacitor/preferences": "^6.0.2", + "@netlify/blobs": "^6.5.0 || ^7.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.0", + "@vercel/kv": "^1.0.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.1" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/untun": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", + "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", + "dev": true, + "dependencies": { + "citty": "^0.1.5", + "consola": "^3.2.3", + "pathe": "^1.1.1" + }, + "bin": { + "untun": "bin/untun.mjs" + } + }, + "node_modules/untyped": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz", + "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", + "dev": true, + "dependencies": { + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", + "@babel/types": "^7.25.7", + "defu": "^6.1.4", + "jiti": "^2.3.1", + "mri": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/untyped/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/unwasm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.9.tgz", + "integrity": "sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==", + "dev": true, + "dependencies": { + "knitwork": "^1.0.0", + "magic-string": "^0.30.8", + "mlly": "^1.6.1", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "unplugin": "^1.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", + "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", + "dev": true + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-hot-client": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.3.tgz", + "integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", + "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", + "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "ansi-escapes": "^4.3.0", + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "fast-glob": "^3.2.7", + "fs-extra": "^11.1.0", + "npm-run-path": "^4.0.1", + "strip-ansi": "^6.0.0", + "tiny-invariant": "^1.1.0", + "vscode-languageclient": "^7.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.2" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=7", + "meow": "^9.0.0", + "optionator": "^0.9.1", + "stylelint": ">=13", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": "~2.1.6" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/vite-plugin-checker/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.7.tgz", + "integrity": "sha512-/XXou3MVc13A5O9/2Nd6xczjrUwt7ZyI9h8pTnUMkr5SshLcb0PJUOVq2V+XVkdeU4njsqAtmK87THZuO2coGA==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "debug": "^4.3.6", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.0.1", + "sirv": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz", + "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true, + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", + "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4", + "semver": "^7.3.4", + "vscode-languageserver-protocol": "3.16.0" + }, + "engines": { + "vscode": "^1.52.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/vue": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-bundle-renderer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.1.tgz", + "integrity": "sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==", + "dev": true, + "dependencies": { + "ufo": "^1.5.4" + } + }, + "node_modules/vue-component-meta": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-component-meta/-/vue-component-meta-1.8.27.tgz", + "integrity": "sha512-j3WJsyQHP4TDlvnjHc/eseo0/eVkf0FaCpkqGwez5zD+Tj31onBzWZEXTnWKs8xRj0n3dMNYdy3SpiS6NubSvg==", + "dev": true, + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "path-browserify": "^1.0.1", + "vue-component-type-helpers": "1.8.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.27.tgz", + "integrity": "sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==", + "dev": true + }, + "node_modules/vue-devtools-stub": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", + "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", + "dev": true + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", + "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100755 index 000000000..b3fc37e86 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,21 @@ +{ + "name": "backrest-docs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "nuxi dev", + "build": "nuxi build", + "generate": "nuxi generate", + "preview": "nuxi preview", + "lint": "eslint ." + }, + "devDependencies": { + "@nuxt-themes/docus": "^1.14.8", + "@nuxt/devtools": "^1.3.1", + "@nuxt/eslint-config": "^0.3.13", + "@nuxtjs/plausible": "^1.0.0", + "@types/node": "^20.12.12", + "eslint": "^8.57.0", + "nuxt": "^3.11.2" + } +} diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 000000000..dfe416355 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/renovate.json b/docs/renovate.json new file mode 100755 index 000000000..f7399e91b --- /dev/null +++ b/docs/renovate.json @@ -0,0 +1,6 @@ +{ + "extends": ["github>nuxt/renovate-config-nuxt"], + "lockFileMaintenance": { + "enabled": true + } +} diff --git a/docs/tokens.config.ts b/docs/tokens.config.ts new file mode 100644 index 000000000..d2d557a91 --- /dev/null +++ b/docs/tokens.config.ts @@ -0,0 +1,3 @@ +import { defineTheme } from "pinceau"; + +export default defineTheme({}); diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100755 index 000000000..4b34df157 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/gen/go/google/api/annotations.pb.go b/gen/go/google/api/annotations.pb.go index d67c95ea4..3c999636c 100644 --- a/gen/go/google/api/annotations.pb.go +++ b/gen/go/google/api/annotations.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: google/api/annotations.proto @@ -77,7 +77,7 @@ var file_google_api_annotations_proto_rawDesc = []byte{ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } -var file_google_api_annotations_proto_goTypes = []interface{}{ +var file_google_api_annotations_proto_goTypes = []any{ (*descriptorpb.MethodOptions)(nil), // 0: google.protobuf.MethodOptions (*HttpRule)(nil), // 1: google.api.HttpRule } diff --git a/gen/go/google/api/http.pb.go b/gen/go/google/api/http.pb.go index 45f95c29f..d6bd4092b 100644 --- a/gen/go/google/api/http.pb.go +++ b/gen/go/google/api/http.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: google/api/http.proto @@ -57,11 +57,9 @@ type Http struct { func (x *Http) Reset() { *x = Http{} - if protoimpl.UnsafeEnabled { - mi := &file_google_api_http_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_google_api_http_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Http) String() string { @@ -72,7 +70,7 @@ func (*Http) ProtoMessage() {} func (x *Http) ProtoReflect() protoreflect.Message { mi := &file_google_api_http_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -417,11 +415,9 @@ type HttpRule struct { func (x *HttpRule) Reset() { *x = HttpRule{} - if protoimpl.UnsafeEnabled { - mi := &file_google_api_http_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_google_api_http_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HttpRule) String() string { @@ -432,7 +428,7 @@ func (*HttpRule) ProtoMessage() {} func (x *HttpRule) ProtoReflect() protoreflect.Message { mi := &file_google_api_http_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -588,11 +584,9 @@ type CustomHttpPattern struct { func (x *CustomHttpPattern) Reset() { *x = CustomHttpPattern{} - if protoimpl.UnsafeEnabled { - mi := &file_google_api_http_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_google_api_http_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CustomHttpPattern) String() string { @@ -603,7 +597,7 @@ func (*CustomHttpPattern) ProtoMessage() {} func (x *CustomHttpPattern) ProtoReflect() protoreflect.Message { mi := &file_google_api_http_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -693,7 +687,7 @@ func file_google_api_http_proto_rawDescGZIP() []byte { } var file_google_api_http_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_google_api_http_proto_goTypes = []interface{}{ +var file_google_api_http_proto_goTypes = []any{ (*Http)(nil), // 0: google.api.Http (*HttpRule)(nil), // 1: google.api.HttpRule (*CustomHttpPattern)(nil), // 2: google.api.CustomHttpPattern @@ -714,45 +708,7 @@ func file_google_api_http_proto_init() { if File_google_api_http_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_google_api_http_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Http); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_google_api_http_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HttpRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_google_api_http_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomHttpPattern); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_google_api_http_proto_msgTypes[1].OneofWrappers = []interface{}{ + file_google_api_http_proto_msgTypes[1].OneofWrappers = []any{ (*HttpRule_Get)(nil), (*HttpRule_Put)(nil), (*HttpRule_Post)(nil), diff --git a/gen/go/types/value.pb.go b/gen/go/types/value.pb.go new file mode 100644 index 000000000..20babd1a0 --- /dev/null +++ b/gen/go/types/value.pb.go @@ -0,0 +1,407 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: types/value.proto + +package types + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type BoolValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value bool `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *BoolValue) Reset() { + *x = BoolValue{} + mi := &file_types_value_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoolValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoolValue) ProtoMessage() {} + +func (x *BoolValue) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoolValue.ProtoReflect.Descriptor instead. +func (*BoolValue) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{0} +} + +func (x *BoolValue) GetValue() bool { + if x != nil { + return x.Value + } + return false +} + +type StringValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *StringValue) Reset() { + *x = StringValue{} + mi := &file_types_value_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringValue) ProtoMessage() {} + +func (x *StringValue) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringValue.ProtoReflect.Descriptor instead. +func (*StringValue) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{1} +} + +func (x *StringValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type BytesValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *BytesValue) Reset() { + *x = BytesValue{} + mi := &file_types_value_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BytesValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BytesValue) ProtoMessage() {} + +func (x *BytesValue) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BytesValue.ProtoReflect.Descriptor instead. +func (*BytesValue) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{2} +} + +func (x *BytesValue) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +type StringList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *StringList) Reset() { + *x = StringList{} + mi := &file_types_value_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringList) ProtoMessage() {} + +func (x *StringList) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringList.ProtoReflect.Descriptor instead. +func (*StringList) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{3} +} + +func (x *StringList) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +type Int64Value struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Int64Value) Reset() { + *x = Int64Value{} + mi := &file_types_value_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Int64Value) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Int64Value) ProtoMessage() {} + +func (x *Int64Value) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Int64Value.ProtoReflect.Descriptor instead. +func (*Int64Value) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{4} +} + +func (x *Int64Value) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + +type Int64List struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []int64 `protobuf:"varint,1,rep,packed,name=values,proto3" json:"values,omitempty"` +} + +func (x *Int64List) Reset() { + *x = Int64List{} + mi := &file_types_value_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Int64List) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Int64List) ProtoMessage() {} + +func (x *Int64List) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Int64List.ProtoReflect.Descriptor instead. +func (*Int64List) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{5} +} + +func (x *Int64List) GetValues() []int64 { + if x != nil { + return x.Values + } + return nil +} + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + mi := &file_types_value_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{6} +} + +var File_types_value_proto protoreflect.FileDescriptor + +var file_types_value_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x21, 0x0a, 0x09, 0x42, 0x6f, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x23, 0x0a, + 0x0b, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x22, 0x0a, 0x0a, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x0a, + 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x22, 0x23, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2f, + 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, + 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, + 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_types_value_proto_rawDescOnce sync.Once + file_types_value_proto_rawDescData = file_types_value_proto_rawDesc +) + +func file_types_value_proto_rawDescGZIP() []byte { + file_types_value_proto_rawDescOnce.Do(func() { + file_types_value_proto_rawDescData = protoimpl.X.CompressGZIP(file_types_value_proto_rawDescData) + }) + return file_types_value_proto_rawDescData +} + +var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_types_value_proto_goTypes = []any{ + (*BoolValue)(nil), // 0: types.BoolValue + (*StringValue)(nil), // 1: types.StringValue + (*BytesValue)(nil), // 2: types.BytesValue + (*StringList)(nil), // 3: types.StringList + (*Int64Value)(nil), // 4: types.Int64Value + (*Int64List)(nil), // 5: types.Int64List + (*Empty)(nil), // 6: types.Empty +} +var file_types_value_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_types_value_proto_init() } +func file_types_value_proto_init() { + if File_types_value_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_types_value_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_types_value_proto_goTypes, + DependencyIndexes: file_types_value_proto_depIdxs, + MessageInfos: file_types_value_proto_msgTypes, + }.Build() + File_types_value_proto = out.File + file_types_value_proto_rawDesc = nil + file_types_value_proto_goTypes = nil + file_types_value_proto_depIdxs = nil +} diff --git a/gen/go/v1/authentication.pb.go b/gen/go/v1/authentication.pb.go new file mode 100644 index 000000000..e04c8fcd3 --- /dev/null +++ b/gen/go/v1/authentication.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: v1/authentication.proto + +package v1 + +import ( + types "github.com/garethgeorge/backrest/gen/go/types" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type LoginRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` +} + +func (x *LoginRequest) Reset() { + *x = LoginRequest{} + mi := &file_v1_authentication_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginRequest) ProtoMessage() {} + +func (x *LoginRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_authentication_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. +func (*LoginRequest) Descriptor() ([]byte, []int) { + return file_v1_authentication_proto_rawDescGZIP(), []int{0} +} + +func (x *LoginRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *LoginRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type LoginResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // JWT token +} + +func (x *LoginResponse) Reset() { + *x = LoginResponse{} + mi := &file_v1_authentication_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginResponse) ProtoMessage() {} + +func (x *LoginResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_authentication_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. +func (*LoginResponse) Descriptor() ([]byte, []int) { + return file_v1_authentication_proto_rawDescGZIP(), []int{1} +} + +func (x *LoginResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +var File_v1_authentication_proto protoreflect.FileDescriptor + +var file_v1_authentication_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, + 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46, 0x0a, 0x0c, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0x7a, 0x0a, 0x0e, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, + 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, + 0x0c, 0x48, 0x61, 0x73, 0x68, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, + 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_authentication_proto_rawDescOnce sync.Once + file_v1_authentication_proto_rawDescData = file_v1_authentication_proto_rawDesc +) + +func file_v1_authentication_proto_rawDescGZIP() []byte { + file_v1_authentication_proto_rawDescOnce.Do(func() { + file_v1_authentication_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_authentication_proto_rawDescData) + }) + return file_v1_authentication_proto_rawDescData +} + +var file_v1_authentication_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_v1_authentication_proto_goTypes = []any{ + (*LoginRequest)(nil), // 0: v1.LoginRequest + (*LoginResponse)(nil), // 1: v1.LoginResponse + (*types.StringValue)(nil), // 2: types.StringValue +} +var file_v1_authentication_proto_depIdxs = []int32{ + 0, // 0: v1.Authentication.Login:input_type -> v1.LoginRequest + 2, // 1: v1.Authentication.HashPassword:input_type -> types.StringValue + 1, // 2: v1.Authentication.Login:output_type -> v1.LoginResponse + 2, // 3: v1.Authentication.HashPassword:output_type -> types.StringValue + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_v1_authentication_proto_init() } +func file_v1_authentication_proto_init() { + if File_v1_authentication_proto != nil { + return + } + file_v1_config_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_authentication_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_v1_authentication_proto_goTypes, + DependencyIndexes: file_v1_authentication_proto_depIdxs, + MessageInfos: file_v1_authentication_proto_msgTypes, + }.Build() + File_v1_authentication_proto = out.File + file_v1_authentication_proto_rawDesc = nil + file_v1_authentication_proto_goTypes = nil + file_v1_authentication_proto_depIdxs = nil +} diff --git a/gen/go/v1/authentication_grpc.pb.go b/gen/go/v1/authentication_grpc.pb.go new file mode 100644 index 000000000..9be66eda6 --- /dev/null +++ b/gen/go/v1/authentication_grpc.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: v1/authentication.proto + +package v1 + +import ( + context "context" + types "github.com/garethgeorge/backrest/gen/go/types" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Authentication_Login_FullMethodName = "/v1.Authentication/Login" + Authentication_HashPassword_FullMethodName = "/v1.Authentication/HashPassword" +) + +// AuthenticationClient is the client API for Authentication service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AuthenticationClient interface { + Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) + HashPassword(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringValue, error) +} + +type authenticationClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthenticationClient(cc grpc.ClientConnInterface) AuthenticationClient { + return &authenticationClient{cc} +} + +func (c *authenticationClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LoginResponse) + err := c.cc.Invoke(ctx, Authentication_Login_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authenticationClient) HashPassword(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringValue, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.StringValue) + err := c.cc.Invoke(ctx, Authentication_HashPassword_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthenticationServer is the server API for Authentication service. +// All implementations must embed UnimplementedAuthenticationServer +// for forward compatibility. +type AuthenticationServer interface { + Login(context.Context, *LoginRequest) (*LoginResponse, error) + HashPassword(context.Context, *types.StringValue) (*types.StringValue, error) + mustEmbedUnimplementedAuthenticationServer() +} + +// UnimplementedAuthenticationServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthenticationServer struct{} + +func (UnimplementedAuthenticationServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedAuthenticationServer) HashPassword(context.Context, *types.StringValue) (*types.StringValue, error) { + return nil, status.Errorf(codes.Unimplemented, "method HashPassword not implemented") +} +func (UnimplementedAuthenticationServer) mustEmbedUnimplementedAuthenticationServer() {} +func (UnimplementedAuthenticationServer) testEmbeddedByValue() {} + +// UnsafeAuthenticationServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthenticationServer will +// result in compilation errors. +type UnsafeAuthenticationServer interface { + mustEmbedUnimplementedAuthenticationServer() +} + +func RegisterAuthenticationServer(s grpc.ServiceRegistrar, srv AuthenticationServer) { + // If the following call pancis, it indicates UnimplementedAuthenticationServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Authentication_ServiceDesc, srv) +} + +func _Authentication_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthenticationServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Authentication_Login_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthenticationServer).Login(ctx, req.(*LoginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Authentication_HashPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthenticationServer).HashPassword(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Authentication_HashPassword_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthenticationServer).HashPassword(ctx, req.(*types.StringValue)) + } + return interceptor(ctx, in, info, handler) +} + +// Authentication_ServiceDesc is the grpc.ServiceDesc for Authentication service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Authentication_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "v1.Authentication", + HandlerType: (*AuthenticationServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Login", + Handler: _Authentication_Login_Handler, + }, + { + MethodName: "HashPassword", + Handler: _Authentication_HashPassword_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "v1/authentication.proto", +} diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 185b180f8..8e0e66b60 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: v1/config.proto @@ -9,6 +9,7 @@ package v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" ) @@ -20,35 +21,1737 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type CommandPrefix_IONiceLevel int32 + +const ( + CommandPrefix_IO_DEFAULT CommandPrefix_IONiceLevel = 0 + CommandPrefix_IO_BEST_EFFORT_LOW CommandPrefix_IONiceLevel = 1 + CommandPrefix_IO_BEST_EFFORT_HIGH CommandPrefix_IONiceLevel = 2 + CommandPrefix_IO_IDLE CommandPrefix_IONiceLevel = 3 +) + +// Enum value maps for CommandPrefix_IONiceLevel. +var ( + CommandPrefix_IONiceLevel_name = map[int32]string{ + 0: "IO_DEFAULT", + 1: "IO_BEST_EFFORT_LOW", + 2: "IO_BEST_EFFORT_HIGH", + 3: "IO_IDLE", + } + CommandPrefix_IONiceLevel_value = map[string]int32{ + "IO_DEFAULT": 0, + "IO_BEST_EFFORT_LOW": 1, + "IO_BEST_EFFORT_HIGH": 2, + "IO_IDLE": 3, + } +) + +func (x CommandPrefix_IONiceLevel) Enum() *CommandPrefix_IONiceLevel { + p := new(CommandPrefix_IONiceLevel) + *p = x + return p +} + +func (x CommandPrefix_IONiceLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CommandPrefix_IONiceLevel) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[0].Descriptor() +} + +func (CommandPrefix_IONiceLevel) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[0] +} + +func (x CommandPrefix_IONiceLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CommandPrefix_IONiceLevel.Descriptor instead. +func (CommandPrefix_IONiceLevel) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{5, 0} +} + +type CommandPrefix_CPUNiceLevel int32 + +const ( + CommandPrefix_CPU_DEFAULT CommandPrefix_CPUNiceLevel = 0 + CommandPrefix_CPU_HIGH CommandPrefix_CPUNiceLevel = 1 + CommandPrefix_CPU_LOW CommandPrefix_CPUNiceLevel = 2 +) + +// Enum value maps for CommandPrefix_CPUNiceLevel. +var ( + CommandPrefix_CPUNiceLevel_name = map[int32]string{ + 0: "CPU_DEFAULT", + 1: "CPU_HIGH", + 2: "CPU_LOW", + } + CommandPrefix_CPUNiceLevel_value = map[string]int32{ + "CPU_DEFAULT": 0, + "CPU_HIGH": 1, + "CPU_LOW": 2, + } +) + +func (x CommandPrefix_CPUNiceLevel) Enum() *CommandPrefix_CPUNiceLevel { + p := new(CommandPrefix_CPUNiceLevel) + *p = x + return p +} + +func (x CommandPrefix_CPUNiceLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CommandPrefix_CPUNiceLevel) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[1].Descriptor() +} + +func (CommandPrefix_CPUNiceLevel) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[1] +} + +func (x CommandPrefix_CPUNiceLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CommandPrefix_CPUNiceLevel.Descriptor instead. +func (CommandPrefix_CPUNiceLevel) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{5, 1} +} + +type Schedule_Clock int32 + +const ( + Schedule_CLOCK_DEFAULT Schedule_Clock = 0 // same as CLOCK_LOCAL + Schedule_CLOCK_LOCAL Schedule_Clock = 1 + Schedule_CLOCK_UTC Schedule_Clock = 2 + Schedule_CLOCK_LAST_RUN_TIME Schedule_Clock = 3 +) + +// Enum value maps for Schedule_Clock. +var ( + Schedule_Clock_name = map[int32]string{ + 0: "CLOCK_DEFAULT", + 1: "CLOCK_LOCAL", + 2: "CLOCK_UTC", + 3: "CLOCK_LAST_RUN_TIME", + } + Schedule_Clock_value = map[string]int32{ + "CLOCK_DEFAULT": 0, + "CLOCK_LOCAL": 1, + "CLOCK_UTC": 2, + "CLOCK_LAST_RUN_TIME": 3, + } +) + +func (x Schedule_Clock) Enum() *Schedule_Clock { + p := new(Schedule_Clock) + *p = x + return p +} + +func (x Schedule_Clock) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Schedule_Clock) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[2].Descriptor() +} + +func (Schedule_Clock) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[2] +} + +func (x Schedule_Clock) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Schedule_Clock.Descriptor instead. +func (Schedule_Clock) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{9, 0} +} + +type Hook_Condition int32 + +const ( + Hook_CONDITION_UNKNOWN Hook_Condition = 0 + Hook_CONDITION_ANY_ERROR Hook_Condition = 1 // error running any operation. + Hook_CONDITION_SNAPSHOT_START Hook_Condition = 2 // backup started. + Hook_CONDITION_SNAPSHOT_END Hook_Condition = 3 // backup completed (success or fail). + Hook_CONDITION_SNAPSHOT_ERROR Hook_Condition = 4 // snapshot failed. + Hook_CONDITION_SNAPSHOT_WARNING Hook_Condition = 5 // snapshot completed with warnings. + Hook_CONDITION_SNAPSHOT_SUCCESS Hook_Condition = 6 // snapshot succeeded. + Hook_CONDITION_SNAPSHOT_SKIPPED Hook_Condition = 7 // snapshot was skipped e.g. due to no changes. + // prune conditions + Hook_CONDITION_PRUNE_START Hook_Condition = 100 // prune started. + Hook_CONDITION_PRUNE_ERROR Hook_Condition = 101 // prune failed. + Hook_CONDITION_PRUNE_SUCCESS Hook_Condition = 102 // prune succeeded. + // check conditions + Hook_CONDITION_CHECK_START Hook_Condition = 200 // check started. + Hook_CONDITION_CHECK_ERROR Hook_Condition = 201 // check failed. + Hook_CONDITION_CHECK_SUCCESS Hook_Condition = 202 // check succeeded. + // forget conditions + Hook_CONDITION_FORGET_START Hook_Condition = 300 // forget started. + Hook_CONDITION_FORGET_ERROR Hook_Condition = 301 // forget failed. + Hook_CONDITION_FORGET_SUCCESS Hook_Condition = 302 // forget succeeded. +) + +// Enum value maps for Hook_Condition. +var ( + Hook_Condition_name = map[int32]string{ + 0: "CONDITION_UNKNOWN", + 1: "CONDITION_ANY_ERROR", + 2: "CONDITION_SNAPSHOT_START", + 3: "CONDITION_SNAPSHOT_END", + 4: "CONDITION_SNAPSHOT_ERROR", + 5: "CONDITION_SNAPSHOT_WARNING", + 6: "CONDITION_SNAPSHOT_SUCCESS", + 7: "CONDITION_SNAPSHOT_SKIPPED", + 100: "CONDITION_PRUNE_START", + 101: "CONDITION_PRUNE_ERROR", + 102: "CONDITION_PRUNE_SUCCESS", + 200: "CONDITION_CHECK_START", + 201: "CONDITION_CHECK_ERROR", + 202: "CONDITION_CHECK_SUCCESS", + 300: "CONDITION_FORGET_START", + 301: "CONDITION_FORGET_ERROR", + 302: "CONDITION_FORGET_SUCCESS", + } + Hook_Condition_value = map[string]int32{ + "CONDITION_UNKNOWN": 0, + "CONDITION_ANY_ERROR": 1, + "CONDITION_SNAPSHOT_START": 2, + "CONDITION_SNAPSHOT_END": 3, + "CONDITION_SNAPSHOT_ERROR": 4, + "CONDITION_SNAPSHOT_WARNING": 5, + "CONDITION_SNAPSHOT_SUCCESS": 6, + "CONDITION_SNAPSHOT_SKIPPED": 7, + "CONDITION_PRUNE_START": 100, + "CONDITION_PRUNE_ERROR": 101, + "CONDITION_PRUNE_SUCCESS": 102, + "CONDITION_CHECK_START": 200, + "CONDITION_CHECK_ERROR": 201, + "CONDITION_CHECK_SUCCESS": 202, + "CONDITION_FORGET_START": 300, + "CONDITION_FORGET_ERROR": 301, + "CONDITION_FORGET_SUCCESS": 302, + } +) + +func (x Hook_Condition) Enum() *Hook_Condition { + p := new(Hook_Condition) + *p = x + return p +} + +func (x Hook_Condition) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Hook_Condition) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[3].Descriptor() +} + +func (Hook_Condition) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[3] +} + +func (x Hook_Condition) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Hook_Condition.Descriptor instead. +func (Hook_Condition) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 0} +} + +type Hook_OnError int32 + +const ( + Hook_ON_ERROR_IGNORE Hook_OnError = 0 + Hook_ON_ERROR_CANCEL Hook_OnError = 1 // cancels the operation and skips subsequent hooks + Hook_ON_ERROR_FATAL Hook_OnError = 2 // fails the operation and subsequent hooks. + Hook_ON_ERROR_RETRY_1MINUTE Hook_OnError = 100 // retry the operation every minute + Hook_ON_ERROR_RETRY_10MINUTES Hook_OnError = 101 // retry the operation every 10 minutes + Hook_ON_ERROR_RETRY_EXPONENTIAL_BACKOFF Hook_OnError = 103 // retry the operation with exponential backoff up to 1h max. +) + +// Enum value maps for Hook_OnError. +var ( + Hook_OnError_name = map[int32]string{ + 0: "ON_ERROR_IGNORE", + 1: "ON_ERROR_CANCEL", + 2: "ON_ERROR_FATAL", + 100: "ON_ERROR_RETRY_1MINUTE", + 101: "ON_ERROR_RETRY_10MINUTES", + 103: "ON_ERROR_RETRY_EXPONENTIAL_BACKOFF", + } + Hook_OnError_value = map[string]int32{ + "ON_ERROR_IGNORE": 0, + "ON_ERROR_CANCEL": 1, + "ON_ERROR_FATAL": 2, + "ON_ERROR_RETRY_1MINUTE": 100, + "ON_ERROR_RETRY_10MINUTES": 101, + "ON_ERROR_RETRY_EXPONENTIAL_BACKOFF": 103, + } +) + +func (x Hook_OnError) Enum() *Hook_OnError { + p := new(Hook_OnError) + *p = x + return p +} + +func (x Hook_OnError) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Hook_OnError) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[4].Descriptor() +} + +func (Hook_OnError) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[4] +} + +func (x Hook_OnError) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Hook_OnError.Descriptor instead. +func (Hook_OnError) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 1} +} + +type Hook_Webhook_Method int32 + +const ( + Hook_Webhook_UNKNOWN Hook_Webhook_Method = 0 + Hook_Webhook_GET Hook_Webhook_Method = 1 + Hook_Webhook_POST Hook_Webhook_Method = 2 +) + +// Enum value maps for Hook_Webhook_Method. +var ( + Hook_Webhook_Method_name = map[int32]string{ + 0: "UNKNOWN", + 1: "GET", + 2: "POST", + } + Hook_Webhook_Method_value = map[string]int32{ + "UNKNOWN": 0, + "GET": 1, + "POST": 2, + } +) + +func (x Hook_Webhook_Method) Enum() *Hook_Webhook_Method { + p := new(Hook_Webhook_Method) + *p = x + return p +} + +func (x Hook_Webhook_Method) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Hook_Webhook_Method) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[5].Descriptor() +} + +func (Hook_Webhook_Method) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[5] +} + +func (x Hook_Webhook_Method) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Hook_Webhook_Method.Descriptor instead. +func (Hook_Webhook_Method) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 1, 0} +} + +type HubConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Instances []*HubConfig_InstanceInfo `protobuf:"bytes,1,rep,name=instances,proto3" json:"instances,omitempty"` +} + +func (x *HubConfig) Reset() { + *x = HubConfig{} + mi := &file_v1_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubConfig) ProtoMessage() {} + +func (x *HubConfig) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HubConfig.ProtoReflect.Descriptor instead. +func (*HubConfig) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{0} +} + +func (x *HubConfig) GetInstances() []*HubConfig_InstanceInfo { + if x != nil { + return x.Instances + } + return nil +} + +// Config is the top level config object for restic UI. type Config struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - LogDir string `protobuf:"bytes,2,opt,name=log_dir,proto3" json:"log_dir,omitempty"` - Repos []*Repo `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"` - Plans []*Plan `protobuf:"bytes,4,rep,name=plans,proto3" json:"plans,omitempty"` + // modification number, used for read-modify-write consistency in the UI. Incremented on every write. + Modno int32 `protobuf:"varint,1,opt,name=modno,proto3" json:"modno,omitempty"` + Version int32 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` // version of the config file format. Used to determine when to run migrations. + // The instance name for the Backrest installation. + // This identifies backups created by this instance and is displayed in the UI. + Instance string `protobuf:"bytes,2,opt,name=instance,proto3" json:"instance,omitempty"` + Repos []*Repo `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"` + Plans []*Plan `protobuf:"bytes,4,rep,name=plans,proto3" json:"plans,omitempty"` + Auth *Auth `protobuf:"bytes,5,opt,name=auth,proto3" json:"auth,omitempty"` + Multihost *Multihost `protobuf:"bytes,7,opt,name=multihost,json=sync,proto3" json:"multihost,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_v1_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetModno() int32 { + if x != nil { + return x.Modno + } + return 0 +} + +func (x *Config) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Config) GetInstance() string { + if x != nil { + return x.Instance + } + return "" +} + +func (x *Config) GetRepos() []*Repo { + if x != nil { + return x.Repos + } + return nil +} + +func (x *Config) GetPlans() []*Plan { + if x != nil { + return x.Plans + } + return nil +} + +func (x *Config) GetAuth() *Auth { + if x != nil { + return x.Auth + } + return nil +} + +func (x *Config) GetMultihost() *Multihost { + if x != nil { + return x.Multihost + } + return nil +} + +type Multihost struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + KnownHosts []*Multihost_Peer `protobuf:"bytes,1,rep,name=known_hosts,json=knownHosts,proto3" json:"known_hosts,omitempty"` + AuthorizedClients []*Multihost_Peer `protobuf:"bytes,2,rep,name=authorized_clients,json=authorizedClients,proto3" json:"authorized_clients,omitempty"` +} + +func (x *Multihost) Reset() { + *x = Multihost{} + mi := &file_v1_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Multihost) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Multihost) ProtoMessage() {} + +func (x *Multihost) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Multihost.ProtoReflect.Descriptor instead. +func (*Multihost) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Multihost) GetKnownHosts() []*Multihost_Peer { + if x != nil { + return x.KnownHosts + } + return nil +} + +func (x *Multihost) GetAuthorizedClients() []*Multihost_Peer { + if x != nil { + return x.AuthorizedClients + } + return nil +} + +type Repo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this repo. + Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"` // URI of the repo. + Guid string `protobuf:"bytes,11,opt,name=guid,proto3" json:"guid,omitempty"` // a globally unique ID for this repo. Should be derived as the 'id' field in `restic cat config --json`. + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` // plaintext password + Env []string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` // extra environment variables to set for restic. + Flags []string `protobuf:"bytes,5,rep,name=flags,proto3" json:"flags,omitempty"` // extra flags set on the restic command. + PrunePolicy *PrunePolicy `protobuf:"bytes,6,opt,name=prune_policy,json=prunePolicy,proto3" json:"prune_policy,omitempty"` // policy for when to run prune. + CheckPolicy *CheckPolicy `protobuf:"bytes,9,opt,name=check_policy,json=checkPolicy,proto3" json:"check_policy,omitempty"` // policy for when to run check. + Hooks []*Hook `protobuf:"bytes,7,rep,name=hooks,proto3" json:"hooks,omitempty"` // hooks to run on events for this repo. + AutoUnlock bool `protobuf:"varint,8,opt,name=auto_unlock,json=autoUnlock,proto3" json:"auto_unlock,omitempty"` // automatically unlock the repo when needed. + AutoInitialize bool `protobuf:"varint,12,opt,name=auto_initialize,json=autoInitialize,proto3" json:"auto_initialize,omitempty"` // whether the repo should be auto-initialized if not found. + CommandPrefix *CommandPrefix `protobuf:"bytes,10,opt,name=command_prefix,json=commandPrefix,proto3" json:"command_prefix,omitempty"` // modifiers for the restic commands + AllowedPeerInstanceIds []string `protobuf:"bytes,100,rep,name=allowed_peer_instance_ids,json=allowedPeers,proto3" json:"allowed_peer_instance_ids,omitempty"` // list of peer instance IDs allowed to access this repo. +} + +func (x *Repo) Reset() { + *x = Repo{} + mi := &file_v1_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Repo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Repo) ProtoMessage() {} + +func (x *Repo) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Repo.ProtoReflect.Descriptor instead. +func (*Repo) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Repo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Repo) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *Repo) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *Repo) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Repo) GetEnv() []string { + if x != nil { + return x.Env + } + return nil +} + +func (x *Repo) GetFlags() []string { + if x != nil { + return x.Flags + } + return nil +} + +func (x *Repo) GetPrunePolicy() *PrunePolicy { + if x != nil { + return x.PrunePolicy + } + return nil +} + +func (x *Repo) GetCheckPolicy() *CheckPolicy { + if x != nil { + return x.CheckPolicy + } + return nil +} + +func (x *Repo) GetHooks() []*Hook { + if x != nil { + return x.Hooks + } + return nil +} + +func (x *Repo) GetAutoUnlock() bool { + if x != nil { + return x.AutoUnlock + } + return false +} + +func (x *Repo) GetAutoInitialize() bool { + if x != nil { + return x.AutoInitialize + } + return false +} + +func (x *Repo) GetCommandPrefix() *CommandPrefix { + if x != nil { + return x.CommandPrefix + } + return nil +} + +func (x *Repo) GetAllowedPeerInstanceIds() []string { + if x != nil { + return x.AllowedPeerInstanceIds + } + return nil +} + +type Plan struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this plan. + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` // ID of the repo to use. + Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` // paths to include in the backup. + Excludes []string `protobuf:"bytes,5,rep,name=excludes,proto3" json:"excludes,omitempty"` // glob patterns to exclude. + Iexcludes []string `protobuf:"bytes,9,rep,name=iexcludes,proto3" json:"iexcludes,omitempty"` // case insensitive glob patterns to exclude. + Schedule *Schedule `protobuf:"bytes,12,opt,name=schedule,proto3" json:"schedule,omitempty"` // schedule for the backup. + Retention *RetentionPolicy `protobuf:"bytes,7,opt,name=retention,proto3" json:"retention,omitempty"` // retention policy for snapshots. + Hooks []*Hook `protobuf:"bytes,8,rep,name=hooks,proto3" json:"hooks,omitempty"` // hooks to run on events for this plan. + BackupFlags []string `protobuf:"bytes,10,rep,name=backup_flags,proto3" json:"backup_flags,omitempty"` // extra flags to set when running a backup command. + SkipIfUnchanged bool `protobuf:"varint,13,opt,name=skip_if_unchanged,json=skipIfUnchanged,proto3" json:"skip_if_unchanged,omitempty"` // skip the backup if no changes are detected. +} + +func (x *Plan) Reset() { + *x = Plan{} + mi := &file_v1_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Plan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plan) ProtoMessage() {} + +func (x *Plan) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plan.ProtoReflect.Descriptor instead. +func (*Plan) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Plan) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Plan) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *Plan) GetPaths() []string { + if x != nil { + return x.Paths + } + return nil +} + +func (x *Plan) GetExcludes() []string { + if x != nil { + return x.Excludes + } + return nil +} + +func (x *Plan) GetIexcludes() []string { + if x != nil { + return x.Iexcludes + } + return nil +} + +func (x *Plan) GetSchedule() *Schedule { + if x != nil { + return x.Schedule + } + return nil +} + +func (x *Plan) GetRetention() *RetentionPolicy { + if x != nil { + return x.Retention + } + return nil +} + +func (x *Plan) GetHooks() []*Hook { + if x != nil { + return x.Hooks + } + return nil +} + +func (x *Plan) GetBackupFlags() []string { + if x != nil { + return x.BackupFlags + } + return nil +} + +func (x *Plan) GetSkipIfUnchanged() bool { + if x != nil { + return x.SkipIfUnchanged + } + return false +} + +type CommandPrefix struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IoNice CommandPrefix_IONiceLevel `protobuf:"varint,1,opt,name=io_nice,json=ioNice,proto3,enum=v1.CommandPrefix_IONiceLevel" json:"io_nice,omitempty"` // ionice level to set. + CpuNice CommandPrefix_CPUNiceLevel `protobuf:"varint,2,opt,name=cpu_nice,json=cpuNice,proto3,enum=v1.CommandPrefix_CPUNiceLevel" json:"cpu_nice,omitempty"` // nice level to set. +} + +func (x *CommandPrefix) Reset() { + *x = CommandPrefix{} + mi := &file_v1_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommandPrefix) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommandPrefix) ProtoMessage() {} + +func (x *CommandPrefix) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommandPrefix.ProtoReflect.Descriptor instead. +func (*CommandPrefix) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{5} +} + +func (x *CommandPrefix) GetIoNice() CommandPrefix_IONiceLevel { + if x != nil { + return x.IoNice + } + return CommandPrefix_IO_DEFAULT +} + +func (x *CommandPrefix) GetCpuNice() CommandPrefix_CPUNiceLevel { + if x != nil { + return x.CpuNice + } + return CommandPrefix_CPU_DEFAULT +} + +type RetentionPolicy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Policy: + // + // *RetentionPolicy_PolicyKeepLastN + // *RetentionPolicy_PolicyTimeBucketed + // *RetentionPolicy_PolicyKeepAll + Policy isRetentionPolicy_Policy `protobuf_oneof:"policy"` +} + +func (x *RetentionPolicy) Reset() { + *x = RetentionPolicy{} + mi := &file_v1_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetentionPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetentionPolicy) ProtoMessage() {} + +func (x *RetentionPolicy) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetentionPolicy.ProtoReflect.Descriptor instead. +func (*RetentionPolicy) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{6} +} + +func (m *RetentionPolicy) GetPolicy() isRetentionPolicy_Policy { + if m != nil { + return m.Policy + } + return nil +} + +func (x *RetentionPolicy) GetPolicyKeepLastN() int32 { + if x, ok := x.GetPolicy().(*RetentionPolicy_PolicyKeepLastN); ok { + return x.PolicyKeepLastN + } + return 0 +} + +func (x *RetentionPolicy) GetPolicyTimeBucketed() *RetentionPolicy_TimeBucketedCounts { + if x, ok := x.GetPolicy().(*RetentionPolicy_PolicyTimeBucketed); ok { + return x.PolicyTimeBucketed + } + return nil +} + +func (x *RetentionPolicy) GetPolicyKeepAll() bool { + if x, ok := x.GetPolicy().(*RetentionPolicy_PolicyKeepAll); ok { + return x.PolicyKeepAll + } + return false +} + +type isRetentionPolicy_Policy interface { + isRetentionPolicy_Policy() +} + +type RetentionPolicy_PolicyKeepLastN struct { + PolicyKeepLastN int32 `protobuf:"varint,10,opt,name=policy_keep_last_n,json=policyKeepLastN,proto3,oneof"` +} + +type RetentionPolicy_PolicyTimeBucketed struct { + PolicyTimeBucketed *RetentionPolicy_TimeBucketedCounts `protobuf:"bytes,11,opt,name=policy_time_bucketed,json=policyTimeBucketed,proto3,oneof"` +} + +type RetentionPolicy_PolicyKeepAll struct { + PolicyKeepAll bool `protobuf:"varint,12,opt,name=policy_keep_all,json=policyKeepAll,proto3,oneof"` +} + +func (*RetentionPolicy_PolicyKeepLastN) isRetentionPolicy_Policy() {} + +func (*RetentionPolicy_PolicyTimeBucketed) isRetentionPolicy_Policy() {} + +func (*RetentionPolicy_PolicyKeepAll) isRetentionPolicy_Policy() {} + +type PrunePolicy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Schedule *Schedule `protobuf:"bytes,2,opt,name=schedule,proto3" json:"schedule,omitempty"` + MaxUnusedBytes int64 `protobuf:"varint,3,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3" json:"max_unused_bytes,omitempty"` // max unused bytes before running prune. + MaxUnusedPercent float64 `protobuf:"fixed64,4,opt,name=max_unused_percent,json=maxUnusedPercent,proto3" json:"max_unused_percent,omitempty"` // max unused percent before running prune. +} + +func (x *PrunePolicy) Reset() { + *x = PrunePolicy{} + mi := &file_v1_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PrunePolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrunePolicy) ProtoMessage() {} + +func (x *PrunePolicy) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrunePolicy.ProtoReflect.Descriptor instead. +func (*PrunePolicy) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{7} +} + +func (x *PrunePolicy) GetSchedule() *Schedule { + if x != nil { + return x.Schedule + } + return nil +} + +func (x *PrunePolicy) GetMaxUnusedBytes() int64 { + if x != nil { + return x.MaxUnusedBytes + } + return 0 +} + +func (x *PrunePolicy) GetMaxUnusedPercent() float64 { + if x != nil { + return x.MaxUnusedPercent + } + return 0 +} + +type CheckPolicy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Schedule *Schedule `protobuf:"bytes,1,opt,name=schedule,proto3" json:"schedule,omitempty"` + // Types that are assignable to Mode: + // + // *CheckPolicy_StructureOnly + // *CheckPolicy_ReadDataSubsetPercent + Mode isCheckPolicy_Mode `protobuf_oneof:"mode"` +} + +func (x *CheckPolicy) Reset() { + *x = CheckPolicy{} + mi := &file_v1_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckPolicy) ProtoMessage() {} + +func (x *CheckPolicy) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckPolicy.ProtoReflect.Descriptor instead. +func (*CheckPolicy) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{8} +} + +func (x *CheckPolicy) GetSchedule() *Schedule { + if x != nil { + return x.Schedule + } + return nil +} + +func (m *CheckPolicy) GetMode() isCheckPolicy_Mode { + if m != nil { + return m.Mode + } + return nil +} + +func (x *CheckPolicy) GetStructureOnly() bool { + if x, ok := x.GetMode().(*CheckPolicy_StructureOnly); ok { + return x.StructureOnly + } + return false +} + +func (x *CheckPolicy) GetReadDataSubsetPercent() float64 { + if x, ok := x.GetMode().(*CheckPolicy_ReadDataSubsetPercent); ok { + return x.ReadDataSubsetPercent + } + return 0 +} + +type isCheckPolicy_Mode interface { + isCheckPolicy_Mode() +} + +type CheckPolicy_StructureOnly struct { + StructureOnly bool `protobuf:"varint,100,opt,name=structure_only,json=structureOnly,proto3,oneof"` // only check the structure of the repo. No pack data is read. +} + +type CheckPolicy_ReadDataSubsetPercent struct { + ReadDataSubsetPercent float64 `protobuf:"fixed64,101,opt,name=read_data_subset_percent,json=readDataSubsetPercent,proto3,oneof"` // check a percentage of pack data. +} + +func (*CheckPolicy_StructureOnly) isCheckPolicy_Mode() {} + +func (*CheckPolicy_ReadDataSubsetPercent) isCheckPolicy_Mode() {} + +type Schedule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Schedule: + // + // *Schedule_Disabled + // *Schedule_Cron + // *Schedule_MaxFrequencyDays + // *Schedule_MaxFrequencyHours + Schedule isSchedule_Schedule `protobuf_oneof:"schedule"` + Clock Schedule_Clock `protobuf:"varint,5,opt,name=clock,proto3,enum=v1.Schedule_Clock" json:"clock,omitempty"` // clock to use for scheduling. +} + +func (x *Schedule) Reset() { + *x = Schedule{} + mi := &file_v1_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schedule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schedule) ProtoMessage() {} + +func (x *Schedule) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schedule.ProtoReflect.Descriptor instead. +func (*Schedule) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{9} +} + +func (m *Schedule) GetSchedule() isSchedule_Schedule { + if m != nil { + return m.Schedule + } + return nil +} + +func (x *Schedule) GetDisabled() bool { + if x, ok := x.GetSchedule().(*Schedule_Disabled); ok { + return x.Disabled + } + return false +} + +func (x *Schedule) GetCron() string { + if x, ok := x.GetSchedule().(*Schedule_Cron); ok { + return x.Cron + } + return "" +} + +func (x *Schedule) GetMaxFrequencyDays() int32 { + if x, ok := x.GetSchedule().(*Schedule_MaxFrequencyDays); ok { + return x.MaxFrequencyDays + } + return 0 +} + +func (x *Schedule) GetMaxFrequencyHours() int32 { + if x, ok := x.GetSchedule().(*Schedule_MaxFrequencyHours); ok { + return x.MaxFrequencyHours + } + return 0 +} + +func (x *Schedule) GetClock() Schedule_Clock { + if x != nil { + return x.Clock + } + return Schedule_CLOCK_DEFAULT +} + +type isSchedule_Schedule interface { + isSchedule_Schedule() +} + +type Schedule_Disabled struct { + Disabled bool `protobuf:"varint,1,opt,name=disabled,proto3,oneof"` // disable the schedule. +} + +type Schedule_Cron struct { + Cron string `protobuf:"bytes,2,opt,name=cron,proto3,oneof"` // cron expression describing the schedule. +} + +type Schedule_MaxFrequencyDays struct { + MaxFrequencyDays int32 `protobuf:"varint,3,opt,name=maxFrequencyDays,proto3,oneof"` // max frequency of runs in days. +} + +type Schedule_MaxFrequencyHours struct { + MaxFrequencyHours int32 `protobuf:"varint,4,opt,name=maxFrequencyHours,proto3,oneof"` // max frequency of runs in hours. +} + +func (*Schedule_Disabled) isSchedule_Schedule() {} + +func (*Schedule_Cron) isSchedule_Schedule() {} + +func (*Schedule_MaxFrequencyDays) isSchedule_Schedule() {} + +func (*Schedule_MaxFrequencyHours) isSchedule_Schedule() {} + +type Hook struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Conditions []Hook_Condition `protobuf:"varint,1,rep,packed,name=conditions,proto3,enum=v1.Hook_Condition" json:"conditions,omitempty"` + OnError Hook_OnError `protobuf:"varint,2,opt,name=on_error,json=onError,proto3,enum=v1.Hook_OnError" json:"on_error,omitempty"` + // Types that are assignable to Action: + // + // *Hook_ActionCommand + // *Hook_ActionWebhook + // *Hook_ActionDiscord + // *Hook_ActionGotify + // *Hook_ActionSlack + // *Hook_ActionShoutrrr + // *Hook_ActionHealthchecks + Action isHook_Action `protobuf_oneof:"action"` +} + +func (x *Hook) Reset() { + *x = Hook{} + mi := &file_v1_config_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook) ProtoMessage() {} + +func (x *Hook) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook.ProtoReflect.Descriptor instead. +func (*Hook) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10} +} + +func (x *Hook) GetConditions() []Hook_Condition { + if x != nil { + return x.Conditions + } + return nil +} + +func (x *Hook) GetOnError() Hook_OnError { + if x != nil { + return x.OnError + } + return Hook_ON_ERROR_IGNORE +} + +func (m *Hook) GetAction() isHook_Action { + if m != nil { + return m.Action + } + return nil +} + +func (x *Hook) GetActionCommand() *Hook_Command { + if x, ok := x.GetAction().(*Hook_ActionCommand); ok { + return x.ActionCommand + } + return nil +} + +func (x *Hook) GetActionWebhook() *Hook_Webhook { + if x, ok := x.GetAction().(*Hook_ActionWebhook); ok { + return x.ActionWebhook + } + return nil +} + +func (x *Hook) GetActionDiscord() *Hook_Discord { + if x, ok := x.GetAction().(*Hook_ActionDiscord); ok { + return x.ActionDiscord + } + return nil +} + +func (x *Hook) GetActionGotify() *Hook_Gotify { + if x, ok := x.GetAction().(*Hook_ActionGotify); ok { + return x.ActionGotify + } + return nil +} + +func (x *Hook) GetActionSlack() *Hook_Slack { + if x, ok := x.GetAction().(*Hook_ActionSlack); ok { + return x.ActionSlack + } + return nil +} + +func (x *Hook) GetActionShoutrrr() *Hook_Shoutrrr { + if x, ok := x.GetAction().(*Hook_ActionShoutrrr); ok { + return x.ActionShoutrrr + } + return nil +} + +func (x *Hook) GetActionHealthchecks() *Hook_Healthchecks { + if x, ok := x.GetAction().(*Hook_ActionHealthchecks); ok { + return x.ActionHealthchecks + } + return nil +} + +type isHook_Action interface { + isHook_Action() +} + +type Hook_ActionCommand struct { + ActionCommand *Hook_Command `protobuf:"bytes,100,opt,name=action_command,json=actionCommand,proto3,oneof"` +} + +type Hook_ActionWebhook struct { + ActionWebhook *Hook_Webhook `protobuf:"bytes,101,opt,name=action_webhook,json=actionWebhook,proto3,oneof"` +} + +type Hook_ActionDiscord struct { + ActionDiscord *Hook_Discord `protobuf:"bytes,102,opt,name=action_discord,json=actionDiscord,proto3,oneof"` +} + +type Hook_ActionGotify struct { + ActionGotify *Hook_Gotify `protobuf:"bytes,103,opt,name=action_gotify,json=actionGotify,proto3,oneof"` +} + +type Hook_ActionSlack struct { + ActionSlack *Hook_Slack `protobuf:"bytes,104,opt,name=action_slack,json=actionSlack,proto3,oneof"` +} + +type Hook_ActionShoutrrr struct { + ActionShoutrrr *Hook_Shoutrrr `protobuf:"bytes,105,opt,name=action_shoutrrr,json=actionShoutrrr,proto3,oneof"` +} + +type Hook_ActionHealthchecks struct { + ActionHealthchecks *Hook_Healthchecks `protobuf:"bytes,106,opt,name=action_healthchecks,json=actionHealthchecks,proto3,oneof"` +} + +func (*Hook_ActionCommand) isHook_Action() {} + +func (*Hook_ActionWebhook) isHook_Action() {} + +func (*Hook_ActionDiscord) isHook_Action() {} + +func (*Hook_ActionGotify) isHook_Action() {} + +func (*Hook_ActionSlack) isHook_Action() {} + +func (*Hook_ActionShoutrrr) isHook_Action() {} + +func (*Hook_ActionHealthchecks) isHook_Action() {} + +type Auth struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Disabled bool `protobuf:"varint,1,opt,name=disabled,proto3" json:"disabled,omitempty"` // disable authentication. + Users []*User `protobuf:"bytes,2,rep,name=users,proto3" json:"users,omitempty"` // users to allow access to the UI. +} + +func (x *Auth) Reset() { + *x = Auth{} + mi := &file_v1_config_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Auth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Auth) ProtoMessage() {} + +func (x *Auth) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Auth.ProtoReflect.Descriptor instead. +func (*Auth) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{11} +} + +func (x *Auth) GetDisabled() bool { + if x != nil { + return x.Disabled + } + return false +} + +func (x *Auth) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +type User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Types that are assignable to Password: + // + // *User_PasswordBcrypt + Password isUser_Password `protobuf_oneof:"password"` +} + +func (x *User) Reset() { + *x = User{} + mi := &file_v1_config_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{12} +} + +func (x *User) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (m *User) GetPassword() isUser_Password { + if m != nil { + return m.Password + } + return nil +} + +func (x *User) GetPasswordBcrypt() string { + if x, ok := x.GetPassword().(*User_PasswordBcrypt); ok { + return x.PasswordBcrypt + } + return "" +} + +type isUser_Password interface { + isUser_Password() +} + +type User_PasswordBcrypt struct { + PasswordBcrypt string `protobuf:"bytes,2,opt,name=password_bcrypt,json=passwordBcrypt,proto3,oneof"` +} + +func (*User_PasswordBcrypt) isUser_Password() {} + +type HubConfig_InstanceInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` // secret used to authenticate with the hub. +} + +func (x *HubConfig_InstanceInfo) Reset() { + *x = HubConfig_InstanceInfo{} + mi := &file_v1_config_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubConfig_InstanceInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubConfig_InstanceInfo) ProtoMessage() {} + +func (x *HubConfig_InstanceInfo) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HubConfig_InstanceInfo.ProtoReflect.Descriptor instead. +func (*HubConfig_InstanceInfo) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *HubConfig_InstanceInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *HubConfig_InstanceInfo) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +type Multihost_Peer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstanceId string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // instance ID of the peer. + PublicKey *PublicKey `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // public key of the peer. If changed, the peer must re-verify the public key. + PublicKeyVerified bool `protobuf:"varint,4,opt,name=public_key_verified,json=publicKeyVerified,proto3" json:"public_key_verified,omitempty"` // whether the public key is verified. This must be set for a host to authenticate a client. Clients implicitly validate the first key they see on initial connection. + // Known host only fields + InstanceUrl string `protobuf:"bytes,2,opt,name=instance_url,json=instanceUrl,proto3" json:"instance_url,omitempty"` // instance URL, required for a known host. Otherwise meaningless. +} + +func (x *Multihost_Peer) Reset() { + *x = Multihost_Peer{} + mi := &file_v1_config_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Multihost_Peer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Multihost_Peer) ProtoMessage() {} + +func (x *Multihost_Peer) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Multihost_Peer.ProtoReflect.Descriptor instead. +func (*Multihost_Peer) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *Multihost_Peer) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *Multihost_Peer) GetPublicKey() *PublicKey { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *Multihost_Peer) GetPublicKeyVerified() bool { + if x != nil { + return x.PublicKeyVerified + } + return false +} + +func (x *Multihost_Peer) GetInstanceUrl() string { + if x != nil { + return x.InstanceUrl + } + return "" +} + +type RetentionPolicy_TimeBucketedCounts struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hourly int32 `protobuf:"varint,1,opt,name=hourly,proto3" json:"hourly,omitempty"` // keep the last n hourly snapshots. + Daily int32 `protobuf:"varint,2,opt,name=daily,proto3" json:"daily,omitempty"` // keep the last n daily snapshots. + Weekly int32 `protobuf:"varint,3,opt,name=weekly,proto3" json:"weekly,omitempty"` // keep the last n weekly snapshots. + Monthly int32 `protobuf:"varint,4,opt,name=monthly,proto3" json:"monthly,omitempty"` // keep the last n monthly snapshots. + Yearly int32 `protobuf:"varint,5,opt,name=yearly,proto3" json:"yearly,omitempty"` // keep the last n yearly snapshots. } -func (x *Config) Reset() { - *x = Config{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[0] +func (x *RetentionPolicy_TimeBucketedCounts) Reset() { + *x = RetentionPolicy_TimeBucketedCounts{} + mi := &file_v1_config_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetentionPolicy_TimeBucketedCounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetentionPolicy_TimeBucketedCounts) ProtoMessage() {} + +func (x *RetentionPolicy_TimeBucketedCounts) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[15] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } + return mi.MessageOf(x) } -func (x *Config) String() string { +// Deprecated: Use RetentionPolicy_TimeBucketedCounts.ProtoReflect.Descriptor instead. +func (*RetentionPolicy_TimeBucketedCounts) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *RetentionPolicy_TimeBucketedCounts) GetHourly() int32 { + if x != nil { + return x.Hourly + } + return 0 +} + +func (x *RetentionPolicy_TimeBucketedCounts) GetDaily() int32 { + if x != nil { + return x.Daily + } + return 0 +} + +func (x *RetentionPolicy_TimeBucketedCounts) GetWeekly() int32 { + if x != nil { + return x.Weekly + } + return 0 +} + +func (x *RetentionPolicy_TimeBucketedCounts) GetMonthly() int32 { + if x != nil { + return x.Monthly + } + return 0 +} + +func (x *RetentionPolicy_TimeBucketedCounts) GetYearly() int32 { + if x != nil { + return x.Yearly + } + return 0 +} + +type Hook_Command struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` +} + +func (x *Hook_Command) Reset() { + *x = Hook_Command{} + mi := &file_v1_config_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Command) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Config) ProtoMessage() {} +func (*Hook_Command) ProtoMessage() {} -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { +func (x *Hook_Command) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[16] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -58,68 +1761,160 @@ func (x *Config) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{0} +// Deprecated: Use Hook_Command.ProtoReflect.Descriptor instead. +func (*Hook_Command) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 0} } -func (x *Config) GetVersion() int32 { +func (x *Hook_Command) GetCommand() string { if x != nil { - return x.Version + return x.Command } - return 0 + return "" +} + +type Hook_Webhook struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WebhookUrl string `protobuf:"bytes,1,opt,name=webhook_url,json=webhookUrl,proto3" json:"webhook_url,omitempty"` + Method Hook_Webhook_Method `protobuf:"varint,2,opt,name=method,proto3,enum=v1.Hook_Webhook_Method" json:"method,omitempty"` + Template string `protobuf:"bytes,100,opt,name=template,proto3" json:"template,omitempty"` +} + +func (x *Hook_Webhook) Reset() { + *x = Hook_Webhook{} + mi := &file_v1_config_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Webhook) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Config) GetLogDir() string { +func (*Hook_Webhook) ProtoMessage() {} + +func (x *Hook_Webhook) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook_Webhook.ProtoReflect.Descriptor instead. +func (*Hook_Webhook) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 1} +} + +func (x *Hook_Webhook) GetWebhookUrl() string { if x != nil { - return x.LogDir + return x.WebhookUrl } return "" } -func (x *Config) GetRepos() []*Repo { +func (x *Hook_Webhook) GetMethod() Hook_Webhook_Method { if x != nil { - return x.Repos + return x.Method } - return nil + return Hook_Webhook_UNKNOWN } -func (x *Config) GetPlans() []*Plan { +func (x *Hook_Webhook) GetTemplate() string { if x != nil { - return x.Plans + return x.Template } - return nil + return "" } -type Repo struct { +type Hook_Discord struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"` - Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` - Env []string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` + WebhookUrl string `protobuf:"bytes,1,opt,name=webhook_url,json=webhookUrl,proto3" json:"webhook_url,omitempty"` + Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` // template for the webhook payload. } -func (x *Repo) Reset() { - *x = Repo{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[1] +func (x *Hook_Discord) Reset() { + *x = Hook_Discord{} + mi := &file_v1_config_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Discord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook_Discord) ProtoMessage() {} + +func (x *Hook_Discord) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[18] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } + return mi.MessageOf(x) } -func (x *Repo) String() string { +// Deprecated: Use Hook_Discord.ProtoReflect.Descriptor instead. +func (*Hook_Discord) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 2} +} + +func (x *Hook_Discord) GetWebhookUrl() string { + if x != nil { + return x.WebhookUrl + } + return "" +} + +func (x *Hook_Discord) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + +type Hook_Gotify struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + BaseUrl string `protobuf:"bytes,1,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + Template string `protobuf:"bytes,100,opt,name=template,proto3" json:"template,omitempty"` // template for the webhook payload. + TitleTemplate string `protobuf:"bytes,101,opt,name=title_template,json=titleTemplate,proto3" json:"title_template,omitempty"` // template for the webhook title. + Priority int32 `protobuf:"varint,102,opt,name=priority,proto3" json:"priority,omitempty"` // priority level for the notification (1-10) +} + +func (x *Hook_Gotify) Reset() { + *x = Hook_Gotify{} + mi := &file_v1_config_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Gotify) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Repo) ProtoMessage() {} +func (*Hook_Gotify) ProtoMessage() {} -func (x *Repo) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { +func (x *Hook_Gotify) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[19] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -129,68 +1924,124 @@ func (x *Repo) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Repo.ProtoReflect.Descriptor instead. -func (*Repo) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{1} +// Deprecated: Use Hook_Gotify.ProtoReflect.Descriptor instead. +func (*Hook_Gotify) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 3} } -func (x *Repo) GetId() string { +func (x *Hook_Gotify) GetBaseUrl() string { if x != nil { - return x.Id + return x.BaseUrl } return "" } -func (x *Repo) GetUri() string { +func (x *Hook_Gotify) GetToken() string { if x != nil { - return x.Uri + return x.Token } return "" } -func (x *Repo) GetPassword() string { +func (x *Hook_Gotify) GetTemplate() string { if x != nil { - return x.Password + return x.Template } return "" } -func (x *Repo) GetEnv() []string { +func (x *Hook_Gotify) GetTitleTemplate() string { if x != nil { - return x.Env + return x.TitleTemplate } - return nil + return "" } -type Plan struct { +func (x *Hook_Gotify) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +type Hook_Slack struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` - RepoPath string `protobuf:"bytes,3,opt,name=repo_path,proto3" json:"repo_path,omitempty"` // subpath of the repo to backup to - Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` + WebhookUrl string `protobuf:"bytes,1,opt,name=webhook_url,json=webhookUrl,proto3" json:"webhook_url,omitempty"` + Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` // template for the webhook payload. } -func (x *Plan) Reset() { - *x = Plan{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[2] +func (x *Hook_Slack) Reset() { + *x = Hook_Slack{} + mi := &file_v1_config_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Slack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook_Slack) ProtoMessage() {} + +func (x *Hook_Slack) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[20] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } + return mi.MessageOf(x) } -func (x *Plan) String() string { +// Deprecated: Use Hook_Slack.ProtoReflect.Descriptor instead. +func (*Hook_Slack) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 4} +} + +func (x *Hook_Slack) GetWebhookUrl() string { + if x != nil { + return x.WebhookUrl + } + return "" +} + +func (x *Hook_Slack) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + +type Hook_Shoutrrr struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ShoutrrrUrl string `protobuf:"bytes,1,opt,name=shoutrrr_url,json=shoutrrrUrl,proto3" json:"shoutrrr_url,omitempty"` + Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` +} + +func (x *Hook_Shoutrrr) Reset() { + *x = Hook_Shoutrrr{} + mi := &file_v1_config_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Shoutrrr) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Plan) ProtoMessage() {} +func (*Hook_Shoutrrr) ProtoMessage() {} -func (x *Plan) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { +func (x *Hook_Shoutrrr) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[21] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -200,66 +2051,383 @@ func (x *Plan) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Plan.ProtoReflect.Descriptor instead. -func (*Plan) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{2} +// Deprecated: Use Hook_Shoutrrr.ProtoReflect.Descriptor instead. +func (*Hook_Shoutrrr) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 5} } -func (x *Plan) GetId() string { +func (x *Hook_Shoutrrr) GetShoutrrrUrl() string { if x != nil { - return x.Id + return x.ShoutrrrUrl } return "" } -func (x *Plan) GetRepo() string { +func (x *Hook_Shoutrrr) GetTemplate() string { if x != nil { - return x.Repo + return x.Template } return "" } -func (x *Plan) GetRepoPath() string { +type Hook_Healthchecks struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WebhookUrl string `protobuf:"bytes,1,opt,name=webhook_url,json=webhookUrl,proto3" json:"webhook_url,omitempty"` + Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` +} + +func (x *Hook_Healthchecks) Reset() { + *x = Hook_Healthchecks{} + mi := &file_v1_config_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_Healthchecks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook_Healthchecks) ProtoMessage() {} + +func (x *Hook_Healthchecks) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook_Healthchecks.ProtoReflect.Descriptor instead. +func (*Hook_Healthchecks) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{10, 6} +} + +func (x *Hook_Healthchecks) GetWebhookUrl() string { if x != nil { - return x.RepoPath + return x.WebhookUrl } return "" } -func (x *Plan) GetPaths() []string { +func (x *Hook_Healthchecks) GetTemplate() string { if x != nil { - return x.Paths + return x.Template } - return nil + return "" } var File_v1_config_proto protoreflect.FileDescriptor var file_v1_config_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0x7c, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, - 0x5f, 0x64, 0x69, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x5f, - 0x64, 0x69, 0x72, 0x12, 0x1e, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x52, 0x05, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x12, 0x1e, 0x0a, 0x05, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x05, 0x70, 0x6c, - 0x61, 0x6e, 0x73, 0x22, 0x56, 0x0a, 0x04, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, - 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x76, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x5e, 0x0a, 0x04, 0x50, - 0x6c, 0x61, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6f, 0x5f, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x70, 0x6f, - 0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x42, 0x2e, 0x5a, 0x2c, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, - 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, - 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x7d, 0x0a, 0x09, 0x48, 0x75, 0x62, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x38, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x75, 0x62, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x36, 0x0a, 0x0c, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x22, 0xda, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x14, 0x0a, + 0x05, 0x6d, 0x6f, 0x64, 0x6e, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6d, 0x6f, + 0x64, 0x6e, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, + 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x12, 0x1e, 0x0a, 0x05, 0x70, 0x6c, 0x61, + 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x05, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x04, 0x61, 0x75, 0x74, + 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, + 0x68, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, + 0x68, 0x6f, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x68, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x73, 0x79, 0x6e, 0x63, 0x22, + 0xae, 0x02, 0x0a, 0x09, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x33, 0x0a, + 0x0b, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x68, 0x6f, 0x73, + 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x0a, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x48, 0x6f, 0x73, + 0x74, 0x73, 0x12, 0x41, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x52, 0x11, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0xa8, 0x01, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1f, + 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x2c, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, + 0x13, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x55, 0x72, 0x6c, + 0x22, 0xbd, 0x03, 0x0a, 0x04, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x67, + 0x75, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, + 0x6e, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x14, 0x0a, + 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x6c, + 0x61, 0x67, 0x73, 0x12, 0x32, 0x0a, 0x0c, 0x70, 0x72, 0x75, 0x6e, 0x65, 0x5f, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x72, 0x75, 0x6e, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x75, 0x6e, + 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x32, 0x0a, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0b, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x0a, 0x05, 0x68, + 0x6f, 0x6f, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, + 0x48, 0x6f, 0x6f, 0x6b, 0x52, 0x05, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, + 0x75, 0x74, 0x6f, 0x5f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x0f, + 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x6f, 0x49, 0x6e, 0x69, 0x74, 0x69, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x12, 0x38, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, + 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, + 0x2f, 0x0a, 0x19, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x64, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x73, + 0x22, 0xd9, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, + 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x14, 0x0a, + 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, + 0x74, 0x68, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x69, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x09, 0x69, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x28, 0x0a, + 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, + 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x31, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, + 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x05, 0x68, 0x6f, + 0x6f, 0x6b, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x48, + 0x6f, 0x6f, 0x6b, 0x52, 0x05, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0c, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, + 0x0a, 0x11, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x69, 0x66, 0x5f, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x49, + 0x66, 0x55, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, + 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0b, 0x10, 0x0c, 0x22, 0x9b, 0x02, 0x0a, + 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x36, + 0x0a, 0x07, 0x69, 0x6f, 0x5f, 0x6e, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x2e, 0x49, 0x4f, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x06, + 0x69, 0x6f, 0x4e, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x5f, 0x6e, 0x69, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2e, 0x43, 0x50, 0x55, 0x4e, + 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x07, 0x63, 0x70, 0x75, 0x4e, 0x69, 0x63, + 0x65, 0x22, 0x5b, 0x0a, 0x0b, 0x49, 0x4f, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x4f, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, + 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4f, 0x5f, 0x42, 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, 0x46, 0x4f, + 0x52, 0x54, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4f, 0x5f, 0x42, + 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, 0x46, 0x4f, 0x52, 0x54, 0x5f, 0x48, 0x49, 0x47, 0x48, 0x10, + 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4f, 0x5f, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x03, 0x22, 0x3a, + 0x0a, 0x0c, 0x43, 0x50, 0x55, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0f, + 0x0a, 0x0b, 0x43, 0x50, 0x55, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, + 0x0c, 0x0a, 0x08, 0x43, 0x50, 0x55, 0x5f, 0x48, 0x49, 0x47, 0x48, 0x10, 0x01, 0x12, 0x0b, 0x0a, + 0x07, 0x43, 0x50, 0x55, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x02, 0x22, 0xdf, 0x02, 0x0a, 0x0f, 0x52, + 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2d, + 0x0a, 0x12, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0f, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x65, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x4e, 0x12, 0x5a, 0x0a, + 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x62, 0x75, 0x63, + 0x6b, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x48, 0x00, 0x52, 0x12, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x69, 0x6d, + 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x6c, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x65, 0x70, + 0x41, 0x6c, 0x6c, 0x1a, 0x8c, 0x01, 0x0a, 0x12, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, + 0x75, 0x72, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x6f, 0x75, 0x72, + 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x05, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x65, 0x6b, + 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x07, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x79, 0x65, + 0x61, 0x72, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x79, 0x65, 0x61, 0x72, + 0x6c, 0x79, 0x42, 0x08, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x8f, 0x01, 0x0a, + 0x0b, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x08, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, + 0x75, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x70, + 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x6d, 0x61, + 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0xa3, + 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, + 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x27, 0x0a, 0x0e, 0x73, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x4f, 0x6e, 0x6c, + 0x79, 0x12, 0x39, 0x0a, 0x18, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, + 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, + 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x42, 0x06, 0x0a, 0x04, + 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa7, 0x02, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, + 0x14, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, + 0x00, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, + 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, + 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, + 0x75, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x53, 0x0a, + 0x05, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, + 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x4c, 0x4f, + 0x43, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4c, + 0x4f, 0x43, 0x4b, 0x5f, 0x55, 0x54, 0x43, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4c, 0x4f, + 0x43, 0x4b, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, + 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0xc5, + 0x0e, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x6f, + 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x07, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x77, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, + 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x39, + 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, + 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, + 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x47, 0x6f, 0x74, 0x69, 0x66, + 0x79, 0x48, 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x6f, 0x74, 0x69, 0x66, + 0x79, 0x12, 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x61, 0x63, + 0x6b, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, + 0x6b, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, + 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, + 0x74, 0x72, 0x72, 0x72, 0x12, 0x48, 0x0a, 0x13, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x48, 0x00, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x1a, 0x23, + 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, + 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, + 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, + 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, + 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, + 0x98, 0x01, 0x0a, 0x06, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, + 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, + 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x66, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, + 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x1a, 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, + 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, + 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x4b, 0x0a, 0x0c, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x77, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xf5, 0x03, 0x0a, 0x09, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, + 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, + 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, + 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, + 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, + 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, + 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, + 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, + 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, 0x1e, + 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, + 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x4b, 0x49, 0x50, 0x50, 0x45, 0x44, 0x10, 0x07, 0x12, 0x19, + 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, + 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, + 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, + 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, + 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, + 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, + 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, + 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x12, 0x1b, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, + 0x54, 0x10, 0xac, 0x02, 0x12, 0x1b, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xad, + 0x02, 0x12, 0x1d, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, + 0x4f, 0x52, 0x47, 0x45, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xae, 0x02, + 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, + 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, + 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, + 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, + 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, + 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, + 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, + 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, + 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, + 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, + 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } @@ -275,20 +2443,78 @@ func file_v1_config_proto_rawDescGZIP() []byte { return file_v1_config_proto_rawDescData } -var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_v1_config_proto_goTypes = []interface{}{ - (*Config)(nil), // 0: v1.Config - (*Repo)(nil), // 1: v1.Repo - (*Plan)(nil), // 2: v1.Plan +var file_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_v1_config_proto_goTypes = []any{ + (CommandPrefix_IONiceLevel)(0), // 0: v1.CommandPrefix.IONiceLevel + (CommandPrefix_CPUNiceLevel)(0), // 1: v1.CommandPrefix.CPUNiceLevel + (Schedule_Clock)(0), // 2: v1.Schedule.Clock + (Hook_Condition)(0), // 3: v1.Hook.Condition + (Hook_OnError)(0), // 4: v1.Hook.OnError + (Hook_Webhook_Method)(0), // 5: v1.Hook.Webhook.Method + (*HubConfig)(nil), // 6: v1.HubConfig + (*Config)(nil), // 7: v1.Config + (*Multihost)(nil), // 8: v1.Multihost + (*Repo)(nil), // 9: v1.Repo + (*Plan)(nil), // 10: v1.Plan + (*CommandPrefix)(nil), // 11: v1.CommandPrefix + (*RetentionPolicy)(nil), // 12: v1.RetentionPolicy + (*PrunePolicy)(nil), // 13: v1.PrunePolicy + (*CheckPolicy)(nil), // 14: v1.CheckPolicy + (*Schedule)(nil), // 15: v1.Schedule + (*Hook)(nil), // 16: v1.Hook + (*Auth)(nil), // 17: v1.Auth + (*User)(nil), // 18: v1.User + (*HubConfig_InstanceInfo)(nil), // 19: v1.HubConfig.InstanceInfo + (*Multihost_Peer)(nil), // 20: v1.Multihost.Peer + (*RetentionPolicy_TimeBucketedCounts)(nil), // 21: v1.RetentionPolicy.TimeBucketedCounts + (*Hook_Command)(nil), // 22: v1.Hook.Command + (*Hook_Webhook)(nil), // 23: v1.Hook.Webhook + (*Hook_Discord)(nil), // 24: v1.Hook.Discord + (*Hook_Gotify)(nil), // 25: v1.Hook.Gotify + (*Hook_Slack)(nil), // 26: v1.Hook.Slack + (*Hook_Shoutrrr)(nil), // 27: v1.Hook.Shoutrrr + (*Hook_Healthchecks)(nil), // 28: v1.Hook.Healthchecks + (*PublicKey)(nil), // 29: v1.PublicKey } var file_v1_config_proto_depIdxs = []int32{ - 1, // 0: v1.Config.repos:type_name -> v1.Repo - 2, // 1: v1.Config.plans:type_name -> v1.Plan - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 19, // 0: v1.HubConfig.instances:type_name -> v1.HubConfig.InstanceInfo + 9, // 1: v1.Config.repos:type_name -> v1.Repo + 10, // 2: v1.Config.plans:type_name -> v1.Plan + 17, // 3: v1.Config.auth:type_name -> v1.Auth + 8, // 4: v1.Config.multihost:type_name -> v1.Multihost + 20, // 5: v1.Multihost.known_hosts:type_name -> v1.Multihost.Peer + 20, // 6: v1.Multihost.authorized_clients:type_name -> v1.Multihost.Peer + 13, // 7: v1.Repo.prune_policy:type_name -> v1.PrunePolicy + 14, // 8: v1.Repo.check_policy:type_name -> v1.CheckPolicy + 16, // 9: v1.Repo.hooks:type_name -> v1.Hook + 11, // 10: v1.Repo.command_prefix:type_name -> v1.CommandPrefix + 15, // 11: v1.Plan.schedule:type_name -> v1.Schedule + 12, // 12: v1.Plan.retention:type_name -> v1.RetentionPolicy + 16, // 13: v1.Plan.hooks:type_name -> v1.Hook + 0, // 14: v1.CommandPrefix.io_nice:type_name -> v1.CommandPrefix.IONiceLevel + 1, // 15: v1.CommandPrefix.cpu_nice:type_name -> v1.CommandPrefix.CPUNiceLevel + 21, // 16: v1.RetentionPolicy.policy_time_bucketed:type_name -> v1.RetentionPolicy.TimeBucketedCounts + 15, // 17: v1.PrunePolicy.schedule:type_name -> v1.Schedule + 15, // 18: v1.CheckPolicy.schedule:type_name -> v1.Schedule + 2, // 19: v1.Schedule.clock:type_name -> v1.Schedule.Clock + 3, // 20: v1.Hook.conditions:type_name -> v1.Hook.Condition + 4, // 21: v1.Hook.on_error:type_name -> v1.Hook.OnError + 22, // 22: v1.Hook.action_command:type_name -> v1.Hook.Command + 23, // 23: v1.Hook.action_webhook:type_name -> v1.Hook.Webhook + 24, // 24: v1.Hook.action_discord:type_name -> v1.Hook.Discord + 25, // 25: v1.Hook.action_gotify:type_name -> v1.Hook.Gotify + 26, // 26: v1.Hook.action_slack:type_name -> v1.Hook.Slack + 27, // 27: v1.Hook.action_shoutrrr:type_name -> v1.Hook.Shoutrrr + 28, // 28: v1.Hook.action_healthchecks:type_name -> v1.Hook.Healthchecks + 18, // 29: v1.Auth.users:type_name -> v1.User + 29, // 30: v1.Multihost.Peer.public_key:type_name -> v1.PublicKey + 5, // 31: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method + 32, // [32:32] is the sub-list for method output_type + 32, // [32:32] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_v1_config_proto_init() } @@ -296,56 +2522,47 @@ func file_v1_config_proto_init() { if File_v1_config_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_v1_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Config); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Repo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Plan); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } + file_v1_crypto_proto_init() + file_v1_config_proto_msgTypes[6].OneofWrappers = []any{ + (*RetentionPolicy_PolicyKeepLastN)(nil), + (*RetentionPolicy_PolicyTimeBucketed)(nil), + (*RetentionPolicy_PolicyKeepAll)(nil), + } + file_v1_config_proto_msgTypes[8].OneofWrappers = []any{ + (*CheckPolicy_StructureOnly)(nil), + (*CheckPolicy_ReadDataSubsetPercent)(nil), + } + file_v1_config_proto_msgTypes[9].OneofWrappers = []any{ + (*Schedule_Disabled)(nil), + (*Schedule_Cron)(nil), + (*Schedule_MaxFrequencyDays)(nil), + (*Schedule_MaxFrequencyHours)(nil), + } + file_v1_config_proto_msgTypes[10].OneofWrappers = []any{ + (*Hook_ActionCommand)(nil), + (*Hook_ActionWebhook)(nil), + (*Hook_ActionDiscord)(nil), + (*Hook_ActionGotify)(nil), + (*Hook_ActionSlack)(nil), + (*Hook_ActionShoutrrr)(nil), + (*Hook_ActionHealthchecks)(nil), + } + file_v1_config_proto_msgTypes[12].OneofWrappers = []any{ + (*User_PasswordBcrypt)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 3, + NumEnums: 6, + NumMessages: 23, NumExtensions: 0, NumServices: 0, }, GoTypes: file_v1_config_proto_goTypes, DependencyIndexes: file_v1_config_proto_depIdxs, + EnumInfos: file_v1_config_proto_enumTypes, MessageInfos: file_v1_config_proto_msgTypes, }.Build() File_v1_config_proto = out.File diff --git a/gen/go/v1/crypto.pb.go b/gen/go/v1/crypto.pb.go new file mode 100644 index 000000000..1351008e8 --- /dev/null +++ b/gen/go/v1/crypto.pb.go @@ -0,0 +1,312 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: v1/crypto.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignedMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Keyid string `protobuf:"bytes,1,opt,name=keyid,proto3" json:"keyid,omitempty"` // a unique identifier generated as the SHA256 of the public key used to sign the message. + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // the payload + Signature []byte `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` // the signature of the payload +} + +func (x *SignedMessage) Reset() { + *x = SignedMessage{} + mi := &file_v1_crypto_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignedMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignedMessage) ProtoMessage() {} + +func (x *SignedMessage) ProtoReflect() protoreflect.Message { + mi := &file_v1_crypto_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignedMessage.ProtoReflect.Descriptor instead. +func (*SignedMessage) Descriptor() ([]byte, []int) { + return file_v1_crypto_proto_rawDescGZIP(), []int{0} +} + +func (x *SignedMessage) GetKeyid() string { + if x != nil { + return x.Keyid + } + return "" +} + +func (x *SignedMessage) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SignedMessage) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +type EncryptedMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` +} + +func (x *EncryptedMessage) Reset() { + *x = EncryptedMessage{} + mi := &file_v1_crypto_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EncryptedMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptedMessage) ProtoMessage() {} + +func (x *EncryptedMessage) ProtoReflect() protoreflect.Message { + mi := &file_v1_crypto_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EncryptedMessage.ProtoReflect.Descriptor instead. +func (*EncryptedMessage) Descriptor() ([]byte, []int) { + return file_v1_crypto_proto_rawDescGZIP(), []int{1} +} + +func (x *EncryptedMessage) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type PublicKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Keyid string `protobuf:"bytes,1,opt,name=keyid,proto3" json:"keyid,omitempty"` // a unique identifier generated as the SHA256 of the public key. + Ed25519 string `protobuf:"bytes,2,opt,name=ed25519,json=ed25519pub,proto3" json:"ed25519,omitempty"` // base64 encoded public key +} + +func (x *PublicKey) Reset() { + *x = PublicKey{} + mi := &file_v1_crypto_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PublicKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PublicKey) ProtoMessage() {} + +func (x *PublicKey) ProtoReflect() protoreflect.Message { + mi := &file_v1_crypto_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PublicKey.ProtoReflect.Descriptor instead. +func (*PublicKey) Descriptor() ([]byte, []int) { + return file_v1_crypto_proto_rawDescGZIP(), []int{2} +} + +func (x *PublicKey) GetKeyid() string { + if x != nil { + return x.Keyid + } + return "" +} + +func (x *PublicKey) GetEd25519() string { + if x != nil { + return x.Ed25519 + } + return "" +} + +type PrivateKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Keyid string `protobuf:"bytes,1,opt,name=keyid,proto3" json:"keyid,omitempty"` // a unique identifier generated as the SHA256 of the public key. + Ed25519 string `protobuf:"bytes,2,opt,name=ed25519,json=ed25519priv,proto3" json:"ed25519,omitempty"` // base64 encoded private key +} + +func (x *PrivateKey) Reset() { + *x = PrivateKey{} + mi := &file_v1_crypto_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PrivateKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrivateKey) ProtoMessage() {} + +func (x *PrivateKey) ProtoReflect() protoreflect.Message { + mi := &file_v1_crypto_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrivateKey.ProtoReflect.Descriptor instead. +func (*PrivateKey) Descriptor() ([]byte, []int) { + return file_v1_crypto_proto_rawDescGZIP(), []int{3} +} + +func (x *PrivateKey) GetKeyid() string { + if x != nil { + return x.Keyid + } + return "" +} + +func (x *PrivateKey) GetEd25519() string { + if x != nil { + return x.Ed25519 + } + return "" +} + +var File_v1_crypto_proto protoreflect.FileDescriptor + +var file_v1_crypto_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0x5d, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6b, 0x65, 0x79, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x22, 0x2c, 0x0a, 0x10, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x22, 0x3e, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x6b, 0x65, 0x79, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x07, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x70, + 0x75, 0x62, 0x22, 0x40, 0x0a, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x6b, 0x65, 0x79, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6b, 0x65, 0x79, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x07, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, + 0x39, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, + 0x70, 0x72, 0x69, 0x76, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, + 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, + 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_crypto_proto_rawDescOnce sync.Once + file_v1_crypto_proto_rawDescData = file_v1_crypto_proto_rawDesc +) + +func file_v1_crypto_proto_rawDescGZIP() []byte { + file_v1_crypto_proto_rawDescOnce.Do(func() { + file_v1_crypto_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_crypto_proto_rawDescData) + }) + return file_v1_crypto_proto_rawDescData +} + +var file_v1_crypto_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_v1_crypto_proto_goTypes = []any{ + (*SignedMessage)(nil), // 0: v1.SignedMessage + (*EncryptedMessage)(nil), // 1: v1.EncryptedMessage + (*PublicKey)(nil), // 2: v1.PublicKey + (*PrivateKey)(nil), // 3: v1.PrivateKey +} +var file_v1_crypto_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_v1_crypto_proto_init() } +func file_v1_crypto_proto_init() { + if File_v1_crypto_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_crypto_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_crypto_proto_goTypes, + DependencyIndexes: file_v1_crypto_proto_depIdxs, + MessageInfos: file_v1_crypto_proto_msgTypes, + }.Build() + File_v1_crypto_proto = out.File + file_v1_crypto_proto_rawDesc = nil + file_v1_crypto_proto_goTypes = nil + file_v1_crypto_proto_depIdxs = nil +} diff --git a/gen/go/v1/events.pb.go b/gen/go/v1/events.pb.go deleted file mode 100644 index be6d7369b..000000000 --- a/gen/go/v1/events.pb.go +++ /dev/null @@ -1,405 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc (unknown) -// source: v1/events.proto - -package v1 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Status int32 - -const ( - Status_UNKNOWN Status = 0 - Status_IN_PROGRESS Status = 1 - Status_SUCCESS Status = 2 - Status_FAILED Status = 3 -) - -// Enum value maps for Status. -var ( - Status_name = map[int32]string{ - 0: "UNKNOWN", - 1: "IN_PROGRESS", - 2: "SUCCESS", - 3: "FAILED", - } - Status_value = map[string]int32{ - "UNKNOWN": 0, - "IN_PROGRESS": 1, - "SUCCESS": 2, - "FAILED": 3, - } -) - -func (x Status) Enum() *Status { - p := new(Status) - *p = x - return p -} - -func (x Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Status) Descriptor() protoreflect.EnumDescriptor { - return file_v1_events_proto_enumTypes[0].Descriptor() -} - -func (Status) Type() protoreflect.EnumType { - return &file_v1_events_proto_enumTypes[0] -} - -func (x Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Status.Descriptor instead. -func (Status) EnumDescriptor() ([]byte, []int) { - return file_v1_events_proto_rawDescGZIP(), []int{0} -} - -type Event struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // timestamp is the number of milliseconds since the Unix epoch. - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // Types that are assignable to Event: - // - // *Event_Log - // *Event_BackupStatusChange - Event isEvent_Event `protobuf_oneof:"event"` -} - -func (x *Event) Reset() { - *x = Event{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_events_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Event) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Event) ProtoMessage() {} - -func (x *Event) ProtoReflect() protoreflect.Message { - mi := &file_v1_events_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Event.ProtoReflect.Descriptor instead. -func (*Event) Descriptor() ([]byte, []int) { - return file_v1_events_proto_rawDescGZIP(), []int{0} -} - -func (x *Event) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (m *Event) GetEvent() isEvent_Event { - if m != nil { - return m.Event - } - return nil -} - -func (x *Event) GetLog() *LogEvent { - if x, ok := x.GetEvent().(*Event_Log); ok { - return x.Log - } - return nil -} - -func (x *Event) GetBackupStatusChange() *BackupStatusEvent { - if x, ok := x.GetEvent().(*Event_BackupStatusChange); ok { - return x.BackupStatusChange - } - return nil -} - -type isEvent_Event interface { - isEvent_Event() -} - -type Event_Log struct { - Log *LogEvent `protobuf:"bytes,3,opt,name=log,proto3,oneof"` -} - -type Event_BackupStatusChange struct { - BackupStatusChange *BackupStatusEvent `protobuf:"bytes,4,opt,name=backup_status_change,json=backup_status,proto3,oneof"` -} - -func (*Event_Log) isEvent_Event() {} - -func (*Event_BackupStatusChange) isEvent_Event() {} - -type LogEvent struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *LogEvent) Reset() { - *x = LogEvent{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_events_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *LogEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LogEvent) ProtoMessage() {} - -func (x *LogEvent) ProtoReflect() protoreflect.Message { - mi := &file_v1_events_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LogEvent.ProtoReflect.Descriptor instead. -func (*LogEvent) Descriptor() ([]byte, []int) { - return file_v1_events_proto_rawDescGZIP(), []int{1} -} - -func (x *LogEvent) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type BackupStatusEvent struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Plan string `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` - Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=v1.Status" json:"status,omitempty"` - Percent uint32 `protobuf:"varint,3,opt,name=percent,proto3" json:"percent,omitempty"` -} - -func (x *BackupStatusEvent) Reset() { - *x = BackupStatusEvent{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_events_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *BackupStatusEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BackupStatusEvent) ProtoMessage() {} - -func (x *BackupStatusEvent) ProtoReflect() protoreflect.Message { - mi := &file_v1_events_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BackupStatusEvent.ProtoReflect.Descriptor instead. -func (*BackupStatusEvent) Descriptor() ([]byte, []int) { - return file_v1_events_proto_rawDescGZIP(), []int{2} -} - -func (x *BackupStatusEvent) GetPlan() string { - if x != nil { - return x.Plan - } - return "" -} - -func (x *BackupStatusEvent) GetStatus() Status { - if x != nil { - return x.Status - } - return Status_UNKNOWN -} - -func (x *BackupStatusEvent) GetPercent() uint32 { - if x != nil { - return x.Percent - } - return 0 -} - -var File_v1_events_proto protoreflect.FileDescriptor - -var file_v1_events_proto_rawDesc = []byte{ - 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0x96, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x20, 0x0a, - 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x6f, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, - 0x44, 0x0a, 0x14, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x24, - 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x65, 0x0a, 0x11, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x22, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0a, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x2a, 0x3f, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, - 0x53, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x02, - 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, 0x2e, 0x5a, 0x2c, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, - 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, - 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_v1_events_proto_rawDescOnce sync.Once - file_v1_events_proto_rawDescData = file_v1_events_proto_rawDesc -) - -func file_v1_events_proto_rawDescGZIP() []byte { - file_v1_events_proto_rawDescOnce.Do(func() { - file_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_events_proto_rawDescData) - }) - return file_v1_events_proto_rawDescData -} - -var file_v1_events_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_v1_events_proto_goTypes = []interface{}{ - (Status)(0), // 0: v1.Status - (*Event)(nil), // 1: v1.Event - (*LogEvent)(nil), // 2: v1.LogEvent - (*BackupStatusEvent)(nil), // 3: v1.BackupStatusEvent -} -var file_v1_events_proto_depIdxs = []int32{ - 2, // 0: v1.Event.log:type_name -> v1.LogEvent - 3, // 1: v1.Event.backup_status_change:type_name -> v1.BackupStatusEvent - 0, // 2: v1.BackupStatusEvent.status:type_name -> v1.Status - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name -} - -func init() { file_v1_events_proto_init() } -func file_v1_events_proto_init() { - if File_v1_events_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_v1_events_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Event); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_events_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogEvent); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_events_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupStatusEvent); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_v1_events_proto_msgTypes[0].OneofWrappers = []interface{}{ - (*Event_Log)(nil), - (*Event_BackupStatusChange)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_v1_events_proto_rawDesc, - NumEnums: 1, - NumMessages: 3, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_v1_events_proto_goTypes, - DependencyIndexes: file_v1_events_proto_depIdxs, - EnumInfos: file_v1_events_proto_enumTypes, - MessageInfos: file_v1_events_proto_msgTypes, - }.Build() - File_v1_events_proto = out.File - file_v1_events_proto_rawDesc = nil - file_v1_events_proto_goTypes = nil - file_v1_events_proto_depIdxs = nil -} diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go new file mode 100644 index 000000000..3e13fb272 --- /dev/null +++ b/gen/go/v1/operations.pb.go @@ -0,0 +1,1413 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: v1/operations.proto + +package v1 + +import ( + types "github.com/garethgeorge/backrest/gen/go/types" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// OperationEventType indicates whether the operation was created or updated +type OperationEventType int32 + +const ( + OperationEventType_EVENT_UNKNOWN OperationEventType = 0 + OperationEventType_EVENT_CREATED OperationEventType = 1 + OperationEventType_EVENT_UPDATED OperationEventType = 2 + OperationEventType_EVENT_DELETED OperationEventType = 3 +) + +// Enum value maps for OperationEventType. +var ( + OperationEventType_name = map[int32]string{ + 0: "EVENT_UNKNOWN", + 1: "EVENT_CREATED", + 2: "EVENT_UPDATED", + 3: "EVENT_DELETED", + } + OperationEventType_value = map[string]int32{ + "EVENT_UNKNOWN": 0, + "EVENT_CREATED": 1, + "EVENT_UPDATED": 2, + "EVENT_DELETED": 3, + } +) + +func (x OperationEventType) Enum() *OperationEventType { + p := new(OperationEventType) + *p = x + return p +} + +func (x OperationEventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (OperationEventType) Descriptor() protoreflect.EnumDescriptor { + return file_v1_operations_proto_enumTypes[0].Descriptor() +} + +func (OperationEventType) Type() protoreflect.EnumType { + return &file_v1_operations_proto_enumTypes[0] +} + +func (x OperationEventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use OperationEventType.Descriptor instead. +func (OperationEventType) EnumDescriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{0} +} + +type OperationStatus int32 + +const ( + OperationStatus_STATUS_UNKNOWN OperationStatus = 0 // used to indicate that the status is unknown. + OperationStatus_STATUS_PENDING OperationStatus = 1 // used to indicate that the operation is pending. + OperationStatus_STATUS_INPROGRESS OperationStatus = 2 // used to indicate that the operation is in progress. + OperationStatus_STATUS_SUCCESS OperationStatus = 3 // used to indicate that the operation completed successfully. + OperationStatus_STATUS_WARNING OperationStatus = 7 // used to indicate that the operation completed with warnings. + OperationStatus_STATUS_ERROR OperationStatus = 4 // used to indicate that the operation failed. + OperationStatus_STATUS_SYSTEM_CANCELLED OperationStatus = 5 // indicates operation cancelled by the system. + OperationStatus_STATUS_USER_CANCELLED OperationStatus = 6 // indicates operation cancelled by the user. +) + +// Enum value maps for OperationStatus. +var ( + OperationStatus_name = map[int32]string{ + 0: "STATUS_UNKNOWN", + 1: "STATUS_PENDING", + 2: "STATUS_INPROGRESS", + 3: "STATUS_SUCCESS", + 7: "STATUS_WARNING", + 4: "STATUS_ERROR", + 5: "STATUS_SYSTEM_CANCELLED", + 6: "STATUS_USER_CANCELLED", + } + OperationStatus_value = map[string]int32{ + "STATUS_UNKNOWN": 0, + "STATUS_PENDING": 1, + "STATUS_INPROGRESS": 2, + "STATUS_SUCCESS": 3, + "STATUS_WARNING": 7, + "STATUS_ERROR": 4, + "STATUS_SYSTEM_CANCELLED": 5, + "STATUS_USER_CANCELLED": 6, + } +) + +func (x OperationStatus) Enum() *OperationStatus { + p := new(OperationStatus) + *p = x + return p +} + +func (x OperationStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (OperationStatus) Descriptor() protoreflect.EnumDescriptor { + return file_v1_operations_proto_enumTypes[1].Descriptor() +} + +func (OperationStatus) Type() protoreflect.EnumType { + return &file_v1_operations_proto_enumTypes[1] +} + +func (x OperationStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use OperationStatus.Descriptor instead. +func (OperationStatus) EnumDescriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{1} +} + +type OperationList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Operations []*Operation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` +} + +func (x *OperationList) Reset() { + *x = OperationList{} + mi := &file_v1_operations_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationList) ProtoMessage() {} + +func (x *OperationList) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationList.ProtoReflect.Descriptor instead. +func (*OperationList) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{0} +} + +func (x *OperationList) GetOperations() []*Operation { + if x != nil { + return x.Operations + } + return nil +} + +type Operation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // required, primary ID of the operation. ID is sequential based on creation time of the operation. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + OriginalId int64 `protobuf:"varint,13,opt,name=original_id,json=originalId,proto3" json:"original_id,omitempty"` + // modno increments with each change to the operation. This supports easy diffing. + Modno int64 `protobuf:"varint,12,opt,name=modno,proto3" json:"modno,omitempty"` + // flow id groups operations together, e.g. by an execution of a plan. + // must be unique within the context of a repo. + FlowId int64 `protobuf:"varint,10,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + OriginalFlowId int64 `protobuf:"varint,14,opt,name=original_flow_id,json=originalFlowId,proto3" json:"original_flow_id,omitempty"` + // repo id is a string identifier for the repo, and repo_guid is the globally unique ID of the repo. + RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + RepoGuid string `protobuf:"bytes,15,opt,name=repo_guid,json=repoGuid,proto3" json:"repo_guid,omitempty"` + // plan id e.g. a scheduled set of operations (or system) that created this operation. + PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + // instance ID that created the operation + InstanceId string `protobuf:"bytes,11,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + // optional snapshot id if associated with a snapshot. + SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` + // required, unix time in milliseconds of the operation's creation (ID is derived from this) + UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"` + // ptional, unix time in milliseconds of the operation's completion + UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"` + // optional, human readable context message, typically an error message. + DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` + // logref can point to arbitrary logs associated with the operation. + Logref string `protobuf:"bytes,9,opt,name=logref,proto3" json:"logref,omitempty"` + // Types that are assignable to Op: + // + // *Operation_OperationBackup + // *Operation_OperationIndexSnapshot + // *Operation_OperationForget + // *Operation_OperationPrune + // *Operation_OperationRestore + // *Operation_OperationStats + // *Operation_OperationRunHook + // *Operation_OperationCheck + // *Operation_OperationRunCommand + Op isOperation_Op `protobuf_oneof:"op"` +} + +func (x *Operation) Reset() { + *x = Operation{} + mi := &file_v1_operations_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Operation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Operation) ProtoMessage() {} + +func (x *Operation) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Operation.ProtoReflect.Descriptor instead. +func (*Operation) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{1} +} + +func (x *Operation) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Operation) GetOriginalId() int64 { + if x != nil { + return x.OriginalId + } + return 0 +} + +func (x *Operation) GetModno() int64 { + if x != nil { + return x.Modno + } + return 0 +} + +func (x *Operation) GetFlowId() int64 { + if x != nil { + return x.FlowId + } + return 0 +} + +func (x *Operation) GetOriginalFlowId() int64 { + if x != nil { + return x.OriginalFlowId + } + return 0 +} + +func (x *Operation) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *Operation) GetRepoGuid() string { + if x != nil { + return x.RepoGuid + } + return "" +} + +func (x *Operation) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +func (x *Operation) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *Operation) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +func (x *Operation) GetStatus() OperationStatus { + if x != nil { + return x.Status + } + return OperationStatus_STATUS_UNKNOWN +} + +func (x *Operation) GetUnixTimeStartMs() int64 { + if x != nil { + return x.UnixTimeStartMs + } + return 0 +} + +func (x *Operation) GetUnixTimeEndMs() int64 { + if x != nil { + return x.UnixTimeEndMs + } + return 0 +} + +func (x *Operation) GetDisplayMessage() string { + if x != nil { + return x.DisplayMessage + } + return "" +} + +func (x *Operation) GetLogref() string { + if x != nil { + return x.Logref + } + return "" +} + +func (m *Operation) GetOp() isOperation_Op { + if m != nil { + return m.Op + } + return nil +} + +func (x *Operation) GetOperationBackup() *OperationBackup { + if x, ok := x.GetOp().(*Operation_OperationBackup); ok { + return x.OperationBackup + } + return nil +} + +func (x *Operation) GetOperationIndexSnapshot() *OperationIndexSnapshot { + if x, ok := x.GetOp().(*Operation_OperationIndexSnapshot); ok { + return x.OperationIndexSnapshot + } + return nil +} + +func (x *Operation) GetOperationForget() *OperationForget { + if x, ok := x.GetOp().(*Operation_OperationForget); ok { + return x.OperationForget + } + return nil +} + +func (x *Operation) GetOperationPrune() *OperationPrune { + if x, ok := x.GetOp().(*Operation_OperationPrune); ok { + return x.OperationPrune + } + return nil +} + +func (x *Operation) GetOperationRestore() *OperationRestore { + if x, ok := x.GetOp().(*Operation_OperationRestore); ok { + return x.OperationRestore + } + return nil +} + +func (x *Operation) GetOperationStats() *OperationStats { + if x, ok := x.GetOp().(*Operation_OperationStats); ok { + return x.OperationStats + } + return nil +} + +func (x *Operation) GetOperationRunHook() *OperationRunHook { + if x, ok := x.GetOp().(*Operation_OperationRunHook); ok { + return x.OperationRunHook + } + return nil +} + +func (x *Operation) GetOperationCheck() *OperationCheck { + if x, ok := x.GetOp().(*Operation_OperationCheck); ok { + return x.OperationCheck + } + return nil +} + +func (x *Operation) GetOperationRunCommand() *OperationRunCommand { + if x, ok := x.GetOp().(*Operation_OperationRunCommand); ok { + return x.OperationRunCommand + } + return nil +} + +type isOperation_Op interface { + isOperation_Op() +} + +type Operation_OperationBackup struct { + OperationBackup *OperationBackup `protobuf:"bytes,100,opt,name=operation_backup,json=operationBackup,proto3,oneof"` +} + +type Operation_OperationIndexSnapshot struct { + OperationIndexSnapshot *OperationIndexSnapshot `protobuf:"bytes,101,opt,name=operation_index_snapshot,json=operationIndexSnapshot,proto3,oneof"` +} + +type Operation_OperationForget struct { + OperationForget *OperationForget `protobuf:"bytes,102,opt,name=operation_forget,json=operationForget,proto3,oneof"` +} + +type Operation_OperationPrune struct { + OperationPrune *OperationPrune `protobuf:"bytes,103,opt,name=operation_prune,json=operationPrune,proto3,oneof"` +} + +type Operation_OperationRestore struct { + OperationRestore *OperationRestore `protobuf:"bytes,104,opt,name=operation_restore,json=operationRestore,proto3,oneof"` +} + +type Operation_OperationStats struct { + OperationStats *OperationStats `protobuf:"bytes,105,opt,name=operation_stats,json=operationStats,proto3,oneof"` +} + +type Operation_OperationRunHook struct { + OperationRunHook *OperationRunHook `protobuf:"bytes,106,opt,name=operation_run_hook,json=operationRunHook,proto3,oneof"` +} + +type Operation_OperationCheck struct { + OperationCheck *OperationCheck `protobuf:"bytes,107,opt,name=operation_check,json=operationCheck,proto3,oneof"` +} + +type Operation_OperationRunCommand struct { + OperationRunCommand *OperationRunCommand `protobuf:"bytes,108,opt,name=operation_run_command,json=operationRunCommand,proto3,oneof"` +} + +func (*Operation_OperationBackup) isOperation_Op() {} + +func (*Operation_OperationIndexSnapshot) isOperation_Op() {} + +func (*Operation_OperationForget) isOperation_Op() {} + +func (*Operation_OperationPrune) isOperation_Op() {} + +func (*Operation_OperationRestore) isOperation_Op() {} + +func (*Operation_OperationStats) isOperation_Op() {} + +func (*Operation_OperationRunHook) isOperation_Op() {} + +func (*Operation_OperationCheck) isOperation_Op() {} + +func (*Operation_OperationRunCommand) isOperation_Op() {} + +// OperationEvent is used in the wireformat to stream operation changes to clients +type OperationEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Event: + // + // *OperationEvent_KeepAlive + // *OperationEvent_CreatedOperations + // *OperationEvent_UpdatedOperations + // *OperationEvent_DeletedOperations + Event isOperationEvent_Event `protobuf_oneof:"event"` +} + +func (x *OperationEvent) Reset() { + *x = OperationEvent{} + mi := &file_v1_operations_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationEvent) ProtoMessage() {} + +func (x *OperationEvent) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationEvent.ProtoReflect.Descriptor instead. +func (*OperationEvent) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{2} +} + +func (m *OperationEvent) GetEvent() isOperationEvent_Event { + if m != nil { + return m.Event + } + return nil +} + +func (x *OperationEvent) GetKeepAlive() *types.Empty { + if x, ok := x.GetEvent().(*OperationEvent_KeepAlive); ok { + return x.KeepAlive + } + return nil +} + +func (x *OperationEvent) GetCreatedOperations() *OperationList { + if x, ok := x.GetEvent().(*OperationEvent_CreatedOperations); ok { + return x.CreatedOperations + } + return nil +} + +func (x *OperationEvent) GetUpdatedOperations() *OperationList { + if x, ok := x.GetEvent().(*OperationEvent_UpdatedOperations); ok { + return x.UpdatedOperations + } + return nil +} + +func (x *OperationEvent) GetDeletedOperations() *types.Int64List { + if x, ok := x.GetEvent().(*OperationEvent_DeletedOperations); ok { + return x.DeletedOperations + } + return nil +} + +type isOperationEvent_Event interface { + isOperationEvent_Event() +} + +type OperationEvent_KeepAlive struct { + KeepAlive *types.Empty `protobuf:"bytes,1,opt,name=keep_alive,json=keepAlive,proto3,oneof"` +} + +type OperationEvent_CreatedOperations struct { + CreatedOperations *OperationList `protobuf:"bytes,2,opt,name=created_operations,json=createdOperations,proto3,oneof"` +} + +type OperationEvent_UpdatedOperations struct { + UpdatedOperations *OperationList `protobuf:"bytes,3,opt,name=updated_operations,json=updatedOperations,proto3,oneof"` +} + +type OperationEvent_DeletedOperations struct { + DeletedOperations *types.Int64List `protobuf:"bytes,4,opt,name=deleted_operations,json=deletedOperations,proto3,oneof"` +} + +func (*OperationEvent_KeepAlive) isOperationEvent_Event() {} + +func (*OperationEvent_CreatedOperations) isOperationEvent_Event() {} + +func (*OperationEvent_UpdatedOperations) isOperationEvent_Event() {} + +func (*OperationEvent_DeletedOperations) isOperationEvent_Event() {} + +type OperationBackup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LastStatus *BackupProgressEntry `protobuf:"bytes,3,opt,name=last_status,json=lastStatus,proto3" json:"last_status,omitempty"` + Errors []*BackupProgressError `protobuf:"bytes,4,rep,name=errors,proto3" json:"errors,omitempty"` +} + +func (x *OperationBackup) Reset() { + *x = OperationBackup{} + mi := &file_v1_operations_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationBackup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationBackup) ProtoMessage() {} + +func (x *OperationBackup) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationBackup.ProtoReflect.Descriptor instead. +func (*OperationBackup) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{3} +} + +func (x *OperationBackup) GetLastStatus() *BackupProgressEntry { + if x != nil { + return x.LastStatus + } + return nil +} + +func (x *OperationBackup) GetErrors() []*BackupProgressError { + if x != nil { + return x.Errors + } + return nil +} + +// OperationIndexSnapshot tracks that a snapshot was detected by backrest. +type OperationIndexSnapshot struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Snapshot *ResticSnapshot `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` // the snapshot that was indexed. + Forgot bool `protobuf:"varint,3,opt,name=forgot,proto3" json:"forgot,omitempty"` // tracks whether this snapshot is forgotten yet. +} + +func (x *OperationIndexSnapshot) Reset() { + *x = OperationIndexSnapshot{} + mi := &file_v1_operations_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationIndexSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationIndexSnapshot) ProtoMessage() {} + +func (x *OperationIndexSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationIndexSnapshot.ProtoReflect.Descriptor instead. +func (*OperationIndexSnapshot) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{4} +} + +func (x *OperationIndexSnapshot) GetSnapshot() *ResticSnapshot { + if x != nil { + return x.Snapshot + } + return nil +} + +func (x *OperationIndexSnapshot) GetForgot() bool { + if x != nil { + return x.Forgot + } + return false +} + +// OperationForget tracks a forget operation. +type OperationForget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Forget []*ResticSnapshot `protobuf:"bytes,1,rep,name=forget,proto3" json:"forget,omitempty"` + Policy *RetentionPolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` +} + +func (x *OperationForget) Reset() { + *x = OperationForget{} + mi := &file_v1_operations_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationForget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationForget) ProtoMessage() {} + +func (x *OperationForget) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationForget.ProtoReflect.Descriptor instead. +func (*OperationForget) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{5} +} + +func (x *OperationForget) GetForget() []*ResticSnapshot { + if x != nil { + return x.Forget + } + return nil +} + +func (x *OperationForget) GetPolicy() *RetentionPolicy { + if x != nil { + return x.Policy + } + return nil +} + +// OperationPrune tracks a prune operation. +type OperationPrune struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Deprecated: Marked as deprecated in v1/operations.proto. + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the prune. + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` // logref of the prune output. +} + +func (x *OperationPrune) Reset() { + *x = OperationPrune{} + mi := &file_v1_operations_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationPrune) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationPrune) ProtoMessage() {} + +func (x *OperationPrune) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationPrune.ProtoReflect.Descriptor instead. +func (*OperationPrune) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{6} +} + +// Deprecated: Marked as deprecated in v1/operations.proto. +func (x *OperationPrune) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *OperationPrune) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + +// OperationCheck tracks a check operation. +type OperationCheck struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Deprecated: Marked as deprecated in v1/operations.proto. + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the check operation. + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` // logref of the check output. +} + +func (x *OperationCheck) Reset() { + *x = OperationCheck{} + mi := &file_v1_operations_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationCheck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationCheck) ProtoMessage() {} + +func (x *OperationCheck) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationCheck.ProtoReflect.Descriptor instead. +func (*OperationCheck) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{7} +} + +// Deprecated: Marked as deprecated in v1/operations.proto. +func (x *OperationCheck) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *OperationCheck) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + +// OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. +type OperationRunCommand struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` + OutputSizeBytes int64 `protobuf:"varint,3,opt,name=output_size_bytes,json=outputSizeBytes,proto3" json:"output_size_bytes,omitempty"` // not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. +} + +func (x *OperationRunCommand) Reset() { + *x = OperationRunCommand{} + mi := &file_v1_operations_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationRunCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationRunCommand) ProtoMessage() {} + +func (x *OperationRunCommand) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationRunCommand.ProtoReflect.Descriptor instead. +func (*OperationRunCommand) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{8} +} + +func (x *OperationRunCommand) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *OperationRunCommand) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + +func (x *OperationRunCommand) GetOutputSizeBytes() int64 { + if x != nil { + return x.OutputSizeBytes + } + return 0 +} + +// OperationRestore tracks a restore operation. +type OperationRestore struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // path in the snapshot to restore. + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` // location to restore it to. + LastStatus *RestoreProgressEntry `protobuf:"bytes,3,opt,name=last_status,json=lastStatus,proto3" json:"last_status,omitempty"` // status of the restore. +} + +func (x *OperationRestore) Reset() { + *x = OperationRestore{} + mi := &file_v1_operations_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationRestore) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationRestore) ProtoMessage() {} + +func (x *OperationRestore) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationRestore.ProtoReflect.Descriptor instead. +func (*OperationRestore) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{9} +} + +func (x *OperationRestore) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *OperationRestore) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *OperationRestore) GetLastStatus() *RestoreProgressEntry { + if x != nil { + return x.LastStatus + } + return nil +} + +// OperationStats tracks a stats operation. +type OperationStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stats *RepoStats `protobuf:"bytes,1,opt,name=stats,proto3" json:"stats,omitempty"` +} + +func (x *OperationStats) Reset() { + *x = OperationStats{} + mi := &file_v1_operations_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationStats) ProtoMessage() {} + +func (x *OperationStats) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationStats.ProtoReflect.Descriptor instead. +func (*OperationStats) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{10} +} + +func (x *OperationStats) GetStats() *RepoStats { + if x != nil { + return x.Stats + } + return nil +} + +// OperationRunHook tracks a hook that was run. +type OperationRunHook struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ParentOp int64 `protobuf:"varint,4,opt,name=parent_op,json=parentOp,proto3" json:"parent_op,omitempty"` // ID of the operation that ran the hook. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // description of the hook that was run. typically repo/hook_idx or plan/hook_idx. + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` // logref of the hook's output. DEPRECATED. + Condition Hook_Condition `protobuf:"varint,3,opt,name=condition,proto3,enum=v1.Hook_Condition" json:"condition,omitempty"` // triggering condition of the hook. +} + +func (x *OperationRunHook) Reset() { + *x = OperationRunHook{} + mi := &file_v1_operations_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationRunHook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationRunHook) ProtoMessage() {} + +func (x *OperationRunHook) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationRunHook.ProtoReflect.Descriptor instead. +func (*OperationRunHook) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{11} +} + +func (x *OperationRunHook) GetParentOp() int64 { + if x != nil { + return x.ParentOp + } + return 0 +} + +func (x *OperationRunHook) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OperationRunHook) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + +func (x *OperationRunHook) GetCondition() Hook_Condition { + if x != nil { + return x.Condition + } + return Hook_CONDITION_UNKNOWN +} + +var File_v1_operations_proto protoreflect.FileDescriptor + +var file_v1_operations_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, + 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, + 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x2d, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xe3, + 0x08, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x6d, 0x6f, 0x64, 0x6e, 0x6f, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x6f, + 0x64, 0x6e, 0x6f, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, + 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, + 0x46, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x67, 0x75, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6f, 0x47, 0x75, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x4d, + 0x73, 0x12, 0x27, 0x0a, 0x10, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, + 0x6e, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x75, 0x6e, 0x69, + 0x78, 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x64, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x40, 0x0a, 0x10, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x48, 0x00, 0x52, 0x0f, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x56, 0x0a, + 0x18, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x48, 0x00, 0x52, 0x16, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x40, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, + 0x72, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x75, 0x6e, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, + 0x72, 0x75, 0x6e, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x68, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x48, 0x00, 0x52, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x69, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x44, 0x0a, 0x12, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x68, 0x6f, 0x6f, 0x6b, + 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x10, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, + 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x48, 0x00, 0x52, + 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, + 0x4d, 0x0a, 0x15, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x75, 0x6e, + 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x13, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x04, + 0x0a, 0x02, 0x6f, 0x70, 0x22, 0x93, 0x02, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x5f, + 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6b, 0x65, 0x65, + 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x42, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x12, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x41, + 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x7c, 0x0a, 0x0f, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x38, 0x0a, + 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, 0x73, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x22, 0x6a, 0x0a, 0x0f, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2a, 0x0a, + 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x1a, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, + 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x1a, 0x0a, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x80, 0x01, 0x0a, + 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x23, + 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, + 0x72, 0x65, 0x66, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, + 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, + 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x60, + 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, + 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, + 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, + 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, + 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, + 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, 0x0a, + 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, + 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, + 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, + 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, + 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_operations_proto_rawDescOnce sync.Once + file_v1_operations_proto_rawDescData = file_v1_operations_proto_rawDesc +) + +func file_v1_operations_proto_rawDescGZIP() []byte { + file_v1_operations_proto_rawDescOnce.Do(func() { + file_v1_operations_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_operations_proto_rawDescData) + }) + return file_v1_operations_proto_rawDescData +} + +var file_v1_operations_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_v1_operations_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_v1_operations_proto_goTypes = []any{ + (OperationEventType)(0), // 0: v1.OperationEventType + (OperationStatus)(0), // 1: v1.OperationStatus + (*OperationList)(nil), // 2: v1.OperationList + (*Operation)(nil), // 3: v1.Operation + (*OperationEvent)(nil), // 4: v1.OperationEvent + (*OperationBackup)(nil), // 5: v1.OperationBackup + (*OperationIndexSnapshot)(nil), // 6: v1.OperationIndexSnapshot + (*OperationForget)(nil), // 7: v1.OperationForget + (*OperationPrune)(nil), // 8: v1.OperationPrune + (*OperationCheck)(nil), // 9: v1.OperationCheck + (*OperationRunCommand)(nil), // 10: v1.OperationRunCommand + (*OperationRestore)(nil), // 11: v1.OperationRestore + (*OperationStats)(nil), // 12: v1.OperationStats + (*OperationRunHook)(nil), // 13: v1.OperationRunHook + (*types.Empty)(nil), // 14: types.Empty + (*types.Int64List)(nil), // 15: types.Int64List + (*BackupProgressEntry)(nil), // 16: v1.BackupProgressEntry + (*BackupProgressError)(nil), // 17: v1.BackupProgressError + (*ResticSnapshot)(nil), // 18: v1.ResticSnapshot + (*RetentionPolicy)(nil), // 19: v1.RetentionPolicy + (*RestoreProgressEntry)(nil), // 20: v1.RestoreProgressEntry + (*RepoStats)(nil), // 21: v1.RepoStats + (Hook_Condition)(0), // 22: v1.Hook.Condition +} +var file_v1_operations_proto_depIdxs = []int32{ + 3, // 0: v1.OperationList.operations:type_name -> v1.Operation + 1, // 1: v1.Operation.status:type_name -> v1.OperationStatus + 5, // 2: v1.Operation.operation_backup:type_name -> v1.OperationBackup + 6, // 3: v1.Operation.operation_index_snapshot:type_name -> v1.OperationIndexSnapshot + 7, // 4: v1.Operation.operation_forget:type_name -> v1.OperationForget + 8, // 5: v1.Operation.operation_prune:type_name -> v1.OperationPrune + 11, // 6: v1.Operation.operation_restore:type_name -> v1.OperationRestore + 12, // 7: v1.Operation.operation_stats:type_name -> v1.OperationStats + 13, // 8: v1.Operation.operation_run_hook:type_name -> v1.OperationRunHook + 9, // 9: v1.Operation.operation_check:type_name -> v1.OperationCheck + 10, // 10: v1.Operation.operation_run_command:type_name -> v1.OperationRunCommand + 14, // 11: v1.OperationEvent.keep_alive:type_name -> types.Empty + 2, // 12: v1.OperationEvent.created_operations:type_name -> v1.OperationList + 2, // 13: v1.OperationEvent.updated_operations:type_name -> v1.OperationList + 15, // 14: v1.OperationEvent.deleted_operations:type_name -> types.Int64List + 16, // 15: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry + 17, // 16: v1.OperationBackup.errors:type_name -> v1.BackupProgressError + 18, // 17: v1.OperationIndexSnapshot.snapshot:type_name -> v1.ResticSnapshot + 18, // 18: v1.OperationForget.forget:type_name -> v1.ResticSnapshot + 19, // 19: v1.OperationForget.policy:type_name -> v1.RetentionPolicy + 20, // 20: v1.OperationRestore.last_status:type_name -> v1.RestoreProgressEntry + 21, // 21: v1.OperationStats.stats:type_name -> v1.RepoStats + 22, // 22: v1.OperationRunHook.condition:type_name -> v1.Hook.Condition + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name +} + +func init() { file_v1_operations_proto_init() } +func file_v1_operations_proto_init() { + if File_v1_operations_proto != nil { + return + } + file_v1_restic_proto_init() + file_v1_config_proto_init() + file_v1_operations_proto_msgTypes[1].OneofWrappers = []any{ + (*Operation_OperationBackup)(nil), + (*Operation_OperationIndexSnapshot)(nil), + (*Operation_OperationForget)(nil), + (*Operation_OperationPrune)(nil), + (*Operation_OperationRestore)(nil), + (*Operation_OperationStats)(nil), + (*Operation_OperationRunHook)(nil), + (*Operation_OperationCheck)(nil), + (*Operation_OperationRunCommand)(nil), + } + file_v1_operations_proto_msgTypes[2].OneofWrappers = []any{ + (*OperationEvent_KeepAlive)(nil), + (*OperationEvent_CreatedOperations)(nil), + (*OperationEvent_UpdatedOperations)(nil), + (*OperationEvent_DeletedOperations)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_operations_proto_rawDesc, + NumEnums: 2, + NumMessages: 12, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_operations_proto_goTypes, + DependencyIndexes: file_v1_operations_proto_depIdxs, + EnumInfos: file_v1_operations_proto_enumTypes, + MessageInfos: file_v1_operations_proto_msgTypes, + }.Build() + File_v1_operations_proto = out.File + file_v1_operations_proto_rawDesc = nil + file_v1_operations_proto_goTypes = nil + file_v1_operations_proto_depIdxs = nil +} diff --git a/gen/go/v1/restic.pb.go b/gen/go/v1/restic.pb.go new file mode 100644 index 000000000..a1e6f2a26 --- /dev/null +++ b/gen/go/v1/restic.pb.go @@ -0,0 +1,1071 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: v1/restic.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ResticSnapshot represents a restic snapshot. +type ResticSnapshot struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + UnixTimeMs int64 `protobuf:"varint,2,opt,name=unix_time_ms,json=unixTimeMs,proto3" json:"unix_time_ms,omitempty"` + Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"` + Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` + Tree string `protobuf:"bytes,5,opt,name=tree,proto3" json:"tree,omitempty"` // tree hash + Parent string `protobuf:"bytes,6,opt,name=parent,proto3" json:"parent,omitempty"` // parent snapshot's id + Paths []string `protobuf:"bytes,7,rep,name=paths,proto3" json:"paths,omitempty"` + Tags []string `protobuf:"bytes,8,rep,name=tags,proto3" json:"tags,omitempty"` + Summary *SnapshotSummary `protobuf:"bytes,9,opt,name=summary,proto3" json:"summary,omitempty"` // added in 0.17.0 restic outputs the summary in the snapshot +} + +func (x *ResticSnapshot) Reset() { + *x = ResticSnapshot{} + mi := &file_v1_restic_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResticSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResticSnapshot) ProtoMessage() {} + +func (x *ResticSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResticSnapshot.ProtoReflect.Descriptor instead. +func (*ResticSnapshot) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{0} +} + +func (x *ResticSnapshot) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ResticSnapshot) GetUnixTimeMs() int64 { + if x != nil { + return x.UnixTimeMs + } + return 0 +} + +func (x *ResticSnapshot) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *ResticSnapshot) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *ResticSnapshot) GetTree() string { + if x != nil { + return x.Tree + } + return "" +} + +func (x *ResticSnapshot) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *ResticSnapshot) GetPaths() []string { + if x != nil { + return x.Paths + } + return nil +} + +func (x *ResticSnapshot) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *ResticSnapshot) GetSummary() *SnapshotSummary { + if x != nil { + return x.Summary + } + return nil +} + +type SnapshotSummary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FilesNew int64 `protobuf:"varint,1,opt,name=files_new,json=filesNew,proto3" json:"files_new,omitempty"` + FilesChanged int64 `protobuf:"varint,2,opt,name=files_changed,json=filesChanged,proto3" json:"files_changed,omitempty"` + FilesUnmodified int64 `protobuf:"varint,3,opt,name=files_unmodified,json=filesUnmodified,proto3" json:"files_unmodified,omitempty"` + DirsNew int64 `protobuf:"varint,4,opt,name=dirs_new,json=dirsNew,proto3" json:"dirs_new,omitempty"` + DirsChanged int64 `protobuf:"varint,5,opt,name=dirs_changed,json=dirsChanged,proto3" json:"dirs_changed,omitempty"` + DirsUnmodified int64 `protobuf:"varint,6,opt,name=dirs_unmodified,json=dirsUnmodified,proto3" json:"dirs_unmodified,omitempty"` + DataBlobs int64 `protobuf:"varint,7,opt,name=data_blobs,json=dataBlobs,proto3" json:"data_blobs,omitempty"` + TreeBlobs int64 `protobuf:"varint,8,opt,name=tree_blobs,json=treeBlobs,proto3" json:"tree_blobs,omitempty"` + DataAdded int64 `protobuf:"varint,9,opt,name=data_added,json=dataAdded,proto3" json:"data_added,omitempty"` + TotalFilesProcessed int64 `protobuf:"varint,10,opt,name=total_files_processed,json=totalFilesProcessed,proto3" json:"total_files_processed,omitempty"` + TotalBytesProcessed int64 `protobuf:"varint,11,opt,name=total_bytes_processed,json=totalBytesProcessed,proto3" json:"total_bytes_processed,omitempty"` + TotalDuration float64 `protobuf:"fixed64,12,opt,name=total_duration,json=totalDuration,proto3" json:"total_duration,omitempty"` +} + +func (x *SnapshotSummary) Reset() { + *x = SnapshotSummary{} + mi := &file_v1_restic_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SnapshotSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotSummary) ProtoMessage() {} + +func (x *SnapshotSummary) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SnapshotSummary.ProtoReflect.Descriptor instead. +func (*SnapshotSummary) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{1} +} + +func (x *SnapshotSummary) GetFilesNew() int64 { + if x != nil { + return x.FilesNew + } + return 0 +} + +func (x *SnapshotSummary) GetFilesChanged() int64 { + if x != nil { + return x.FilesChanged + } + return 0 +} + +func (x *SnapshotSummary) GetFilesUnmodified() int64 { + if x != nil { + return x.FilesUnmodified + } + return 0 +} + +func (x *SnapshotSummary) GetDirsNew() int64 { + if x != nil { + return x.DirsNew + } + return 0 +} + +func (x *SnapshotSummary) GetDirsChanged() int64 { + if x != nil { + return x.DirsChanged + } + return 0 +} + +func (x *SnapshotSummary) GetDirsUnmodified() int64 { + if x != nil { + return x.DirsUnmodified + } + return 0 +} + +func (x *SnapshotSummary) GetDataBlobs() int64 { + if x != nil { + return x.DataBlobs + } + return 0 +} + +func (x *SnapshotSummary) GetTreeBlobs() int64 { + if x != nil { + return x.TreeBlobs + } + return 0 +} + +func (x *SnapshotSummary) GetDataAdded() int64 { + if x != nil { + return x.DataAdded + } + return 0 +} + +func (x *SnapshotSummary) GetTotalFilesProcessed() int64 { + if x != nil { + return x.TotalFilesProcessed + } + return 0 +} + +func (x *SnapshotSummary) GetTotalBytesProcessed() int64 { + if x != nil { + return x.TotalBytesProcessed + } + return 0 +} + +func (x *SnapshotSummary) GetTotalDuration() float64 { + if x != nil { + return x.TotalDuration + } + return 0 +} + +// ResticSnapshotList represents a list of restic snapshots. +type ResticSnapshotList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Snapshots []*ResticSnapshot `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"` +} + +func (x *ResticSnapshotList) Reset() { + *x = ResticSnapshotList{} + mi := &file_v1_restic_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResticSnapshotList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResticSnapshotList) ProtoMessage() {} + +func (x *ResticSnapshotList) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResticSnapshotList.ProtoReflect.Descriptor instead. +func (*ResticSnapshotList) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{2} +} + +func (x *ResticSnapshotList) GetSnapshots() []*ResticSnapshot { + if x != nil { + return x.Snapshots + } + return nil +} + +// BackupProgressEntriy represents a single entry in the backup progress stream. +type BackupProgressEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Entry: + // + // *BackupProgressEntry_Status + // *BackupProgressEntry_Summary + Entry isBackupProgressEntry_Entry `protobuf_oneof:"entry"` +} + +func (x *BackupProgressEntry) Reset() { + *x = BackupProgressEntry{} + mi := &file_v1_restic_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackupProgressEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackupProgressEntry) ProtoMessage() {} + +func (x *BackupProgressEntry) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackupProgressEntry.ProtoReflect.Descriptor instead. +func (*BackupProgressEntry) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{3} +} + +func (m *BackupProgressEntry) GetEntry() isBackupProgressEntry_Entry { + if m != nil { + return m.Entry + } + return nil +} + +func (x *BackupProgressEntry) GetStatus() *BackupProgressStatusEntry { + if x, ok := x.GetEntry().(*BackupProgressEntry_Status); ok { + return x.Status + } + return nil +} + +func (x *BackupProgressEntry) GetSummary() *BackupProgressSummary { + if x, ok := x.GetEntry().(*BackupProgressEntry_Summary); ok { + return x.Summary + } + return nil +} + +type isBackupProgressEntry_Entry interface { + isBackupProgressEntry_Entry() +} + +type BackupProgressEntry_Status struct { + Status *BackupProgressStatusEntry `protobuf:"bytes,1,opt,name=status,proto3,oneof"` +} + +type BackupProgressEntry_Summary struct { + Summary *BackupProgressSummary `protobuf:"bytes,2,opt,name=summary,proto3,oneof"` +} + +func (*BackupProgressEntry_Status) isBackupProgressEntry_Entry() {} + +func (*BackupProgressEntry_Summary) isBackupProgressEntry_Entry() {} + +// BackupProgressStatusEntry represents a single status entry in the backup progress stream. +type BackupProgressStatusEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // See https://restic.readthedocs.io/en/stable/075_scripting.html#id1 + PercentDone float64 `protobuf:"fixed64,1,opt,name=percent_done,json=percentDone,proto3" json:"percent_done,omitempty"` // 0.0 - 1.0 + TotalFiles int64 `protobuf:"varint,2,opt,name=total_files,json=totalFiles,proto3" json:"total_files,omitempty"` + TotalBytes int64 `protobuf:"varint,3,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"` + FilesDone int64 `protobuf:"varint,4,opt,name=files_done,json=filesDone,proto3" json:"files_done,omitempty"` + BytesDone int64 `protobuf:"varint,5,opt,name=bytes_done,json=bytesDone,proto3" json:"bytes_done,omitempty"` + CurrentFile []string `protobuf:"bytes,6,rep,name=current_file,json=currentFile,proto3" json:"current_file,omitempty"` +} + +func (x *BackupProgressStatusEntry) Reset() { + *x = BackupProgressStatusEntry{} + mi := &file_v1_restic_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackupProgressStatusEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackupProgressStatusEntry) ProtoMessage() {} + +func (x *BackupProgressStatusEntry) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackupProgressStatusEntry.ProtoReflect.Descriptor instead. +func (*BackupProgressStatusEntry) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{4} +} + +func (x *BackupProgressStatusEntry) GetPercentDone() float64 { + if x != nil { + return x.PercentDone + } + return 0 +} + +func (x *BackupProgressStatusEntry) GetTotalFiles() int64 { + if x != nil { + return x.TotalFiles + } + return 0 +} + +func (x *BackupProgressStatusEntry) GetTotalBytes() int64 { + if x != nil { + return x.TotalBytes + } + return 0 +} + +func (x *BackupProgressStatusEntry) GetFilesDone() int64 { + if x != nil { + return x.FilesDone + } + return 0 +} + +func (x *BackupProgressStatusEntry) GetBytesDone() int64 { + if x != nil { + return x.BytesDone + } + return 0 +} + +func (x *BackupProgressStatusEntry) GetCurrentFile() []string { + if x != nil { + return x.CurrentFile + } + return nil +} + +// BackupProgressSummary represents a the summary event emitted at the end of a backup stream. +type BackupProgressSummary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // See https://restic.readthedocs.io/en/stable/075_scripting.html#summary + FilesNew int64 `protobuf:"varint,1,opt,name=files_new,json=filesNew,proto3" json:"files_new,omitempty"` + FilesChanged int64 `protobuf:"varint,2,opt,name=files_changed,json=filesChanged,proto3" json:"files_changed,omitempty"` + FilesUnmodified int64 `protobuf:"varint,3,opt,name=files_unmodified,json=filesUnmodified,proto3" json:"files_unmodified,omitempty"` + DirsNew int64 `protobuf:"varint,4,opt,name=dirs_new,json=dirsNew,proto3" json:"dirs_new,omitempty"` + DirsChanged int64 `protobuf:"varint,5,opt,name=dirs_changed,json=dirsChanged,proto3" json:"dirs_changed,omitempty"` + DirsUnmodified int64 `protobuf:"varint,6,opt,name=dirs_unmodified,json=dirsUnmodified,proto3" json:"dirs_unmodified,omitempty"` + DataBlobs int64 `protobuf:"varint,7,opt,name=data_blobs,json=dataBlobs,proto3" json:"data_blobs,omitempty"` + TreeBlobs int64 `protobuf:"varint,8,opt,name=tree_blobs,json=treeBlobs,proto3" json:"tree_blobs,omitempty"` + DataAdded int64 `protobuf:"varint,9,opt,name=data_added,json=dataAdded,proto3" json:"data_added,omitempty"` + TotalFilesProcessed int64 `protobuf:"varint,10,opt,name=total_files_processed,json=totalFilesProcessed,proto3" json:"total_files_processed,omitempty"` + TotalBytesProcessed int64 `protobuf:"varint,11,opt,name=total_bytes_processed,json=totalBytesProcessed,proto3" json:"total_bytes_processed,omitempty"` + TotalDuration float64 `protobuf:"fixed64,12,opt,name=total_duration,json=totalDuration,proto3" json:"total_duration,omitempty"` + SnapshotId string `protobuf:"bytes,13,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` +} + +func (x *BackupProgressSummary) Reset() { + *x = BackupProgressSummary{} + mi := &file_v1_restic_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackupProgressSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackupProgressSummary) ProtoMessage() {} + +func (x *BackupProgressSummary) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackupProgressSummary.ProtoReflect.Descriptor instead. +func (*BackupProgressSummary) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{5} +} + +func (x *BackupProgressSummary) GetFilesNew() int64 { + if x != nil { + return x.FilesNew + } + return 0 +} + +func (x *BackupProgressSummary) GetFilesChanged() int64 { + if x != nil { + return x.FilesChanged + } + return 0 +} + +func (x *BackupProgressSummary) GetFilesUnmodified() int64 { + if x != nil { + return x.FilesUnmodified + } + return 0 +} + +func (x *BackupProgressSummary) GetDirsNew() int64 { + if x != nil { + return x.DirsNew + } + return 0 +} + +func (x *BackupProgressSummary) GetDirsChanged() int64 { + if x != nil { + return x.DirsChanged + } + return 0 +} + +func (x *BackupProgressSummary) GetDirsUnmodified() int64 { + if x != nil { + return x.DirsUnmodified + } + return 0 +} + +func (x *BackupProgressSummary) GetDataBlobs() int64 { + if x != nil { + return x.DataBlobs + } + return 0 +} + +func (x *BackupProgressSummary) GetTreeBlobs() int64 { + if x != nil { + return x.TreeBlobs + } + return 0 +} + +func (x *BackupProgressSummary) GetDataAdded() int64 { + if x != nil { + return x.DataAdded + } + return 0 +} + +func (x *BackupProgressSummary) GetTotalFilesProcessed() int64 { + if x != nil { + return x.TotalFilesProcessed + } + return 0 +} + +func (x *BackupProgressSummary) GetTotalBytesProcessed() int64 { + if x != nil { + return x.TotalBytesProcessed + } + return 0 +} + +func (x *BackupProgressSummary) GetTotalDuration() float64 { + if x != nil { + return x.TotalDuration + } + return 0 +} + +func (x *BackupProgressSummary) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +type BackupProgressError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // See https://restic.readthedocs.io/en/stable/075_scripting.html#error + Item string `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + During string `protobuf:"bytes,2,opt,name=during,proto3" json:"during,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *BackupProgressError) Reset() { + *x = BackupProgressError{} + mi := &file_v1_restic_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackupProgressError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackupProgressError) ProtoMessage() {} + +func (x *BackupProgressError) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackupProgressError.ProtoReflect.Descriptor instead. +func (*BackupProgressError) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{6} +} + +func (x *BackupProgressError) GetItem() string { + if x != nil { + return x.Item + } + return "" +} + +func (x *BackupProgressError) GetDuring() string { + if x != nil { + return x.During + } + return "" +} + +func (x *BackupProgressError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// RestoreProgressEvent represents a single entry in the restore progress stream. +type RestoreProgressEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` // "summary" or "status" + SecondsElapsed float64 `protobuf:"fixed64,2,opt,name=seconds_elapsed,json=secondsElapsed,proto3" json:"seconds_elapsed,omitempty"` + TotalBytes int64 `protobuf:"varint,3,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"` + BytesRestored int64 `protobuf:"varint,4,opt,name=bytes_restored,json=bytesRestored,proto3" json:"bytes_restored,omitempty"` + TotalFiles int64 `protobuf:"varint,5,opt,name=total_files,json=totalFiles,proto3" json:"total_files,omitempty"` + FilesRestored int64 `protobuf:"varint,6,opt,name=files_restored,json=filesRestored,proto3" json:"files_restored,omitempty"` + PercentDone float64 `protobuf:"fixed64,7,opt,name=percent_done,json=percentDone,proto3" json:"percent_done,omitempty"` // 0.0 - 1.0 +} + +func (x *RestoreProgressEntry) Reset() { + *x = RestoreProgressEntry{} + mi := &file_v1_restic_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreProgressEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreProgressEntry) ProtoMessage() {} + +func (x *RestoreProgressEntry) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreProgressEntry.ProtoReflect.Descriptor instead. +func (*RestoreProgressEntry) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{7} +} + +func (x *RestoreProgressEntry) GetMessageType() string { + if x != nil { + return x.MessageType + } + return "" +} + +func (x *RestoreProgressEntry) GetSecondsElapsed() float64 { + if x != nil { + return x.SecondsElapsed + } + return 0 +} + +func (x *RestoreProgressEntry) GetTotalBytes() int64 { + if x != nil { + return x.TotalBytes + } + return 0 +} + +func (x *RestoreProgressEntry) GetBytesRestored() int64 { + if x != nil { + return x.BytesRestored + } + return 0 +} + +func (x *RestoreProgressEntry) GetTotalFiles() int64 { + if x != nil { + return x.TotalFiles + } + return 0 +} + +func (x *RestoreProgressEntry) GetFilesRestored() int64 { + if x != nil { + return x.FilesRestored + } + return 0 +} + +func (x *RestoreProgressEntry) GetPercentDone() float64 { + if x != nil { + return x.PercentDone + } + return 0 +} + +type RepoStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TotalSize int64 `protobuf:"varint,1,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + TotalUncompressedSize int64 `protobuf:"varint,2,opt,name=total_uncompressed_size,json=totalUncompressedSize,proto3" json:"total_uncompressed_size,omitempty"` + CompressionRatio float64 `protobuf:"fixed64,3,opt,name=compression_ratio,json=compressionRatio,proto3" json:"compression_ratio,omitempty"` + TotalBlobCount int64 `protobuf:"varint,5,opt,name=total_blob_count,json=totalBlobCount,proto3" json:"total_blob_count,omitempty"` + SnapshotCount int64 `protobuf:"varint,6,opt,name=snapshot_count,json=snapshotCount,proto3" json:"snapshot_count,omitempty"` +} + +func (x *RepoStats) Reset() { + *x = RepoStats{} + mi := &file_v1_restic_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RepoStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepoStats) ProtoMessage() {} + +func (x *RepoStats) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepoStats.ProtoReflect.Descriptor instead. +func (*RepoStats) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{8} +} + +func (x *RepoStats) GetTotalSize() int64 { + if x != nil { + return x.TotalSize + } + return 0 +} + +func (x *RepoStats) GetTotalUncompressedSize() int64 { + if x != nil { + return x.TotalUncompressedSize + } + return 0 +} + +func (x *RepoStats) GetCompressionRatio() float64 { + if x != nil { + return x.CompressionRatio + } + return 0 +} + +func (x *RepoStats) GetTotalBlobCount() int64 { + if x != nil { + return x.TotalBlobCount + } + return 0 +} + +func (x *RepoStats) GetSnapshotCount() int64 { + if x != nil { + return x.SnapshotCount + } + return 0 +} + +var File_v1_restic_proto protoreflect.FileDescriptor + +var file_v1_restic_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0xff, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x75, 0x6e, 0x69, 0x78, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, + 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x72, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x72, 0x65, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x14, + 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, + 0x61, 0x74, 0x68, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x07, + 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0xd1, 0x03, 0x0a, 0x0f, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x65, + 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x29, 0x0a, + 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x55, 0x6e, + 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x69, 0x72, 0x73, + 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x64, 0x69, 0x72, 0x73, + 0x4e, 0x65, 0x77, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, 0x69, 0x72, 0x73, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x75, + 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x64, 0x69, 0x72, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, + 0x0a, 0x0a, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x74, 0x72, 0x65, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, 0x64, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0d, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x46, 0x0a, 0x12, 0x52, + 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x30, 0x0a, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x73, 0x22, 0x8e, 0x01, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x31, + 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x48, 0x00, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x42, 0x07, 0x0a, 0x05, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x22, 0xe1, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x6f, + 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x73, + 0x5f, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x22, 0xf8, 0x03, 0x0a, 0x15, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4e, 0x65, 0x77, 0x12, + 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x75, 0x6e, + 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x64, 0x69, 0x72, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, + 0x72, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x64, 0x69, 0x72, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, 0x0a, + 0x0f, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x64, 0x69, 0x72, 0x73, 0x55, 0x6e, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x62, + 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, + 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x6c, + 0x6f, 0x62, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x72, 0x65, 0x65, 0x42, + 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x61, 0x64, 0x64, + 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, + 0x64, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x50, 0x72, + 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x49, 0x64, 0x22, 0x5b, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, + 0x0a, 0x06, 0x64, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x64, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x95, 0x02, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x45, 0x6c, + 0x61, 0x70, 0x73, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x25, + 0x0a, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x5f, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x44, 0x6f, 0x6e, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x70, + 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x75, + 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x6e, 0x63, + 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, + 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x62, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, + 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_v1_restic_proto_rawDescOnce sync.Once + file_v1_restic_proto_rawDescData = file_v1_restic_proto_rawDesc +) + +func file_v1_restic_proto_rawDescGZIP() []byte { + file_v1_restic_proto_rawDescOnce.Do(func() { + file_v1_restic_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_restic_proto_rawDescData) + }) + return file_v1_restic_proto_rawDescData +} + +var file_v1_restic_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_v1_restic_proto_goTypes = []any{ + (*ResticSnapshot)(nil), // 0: v1.ResticSnapshot + (*SnapshotSummary)(nil), // 1: v1.SnapshotSummary + (*ResticSnapshotList)(nil), // 2: v1.ResticSnapshotList + (*BackupProgressEntry)(nil), // 3: v1.BackupProgressEntry + (*BackupProgressStatusEntry)(nil), // 4: v1.BackupProgressStatusEntry + (*BackupProgressSummary)(nil), // 5: v1.BackupProgressSummary + (*BackupProgressError)(nil), // 6: v1.BackupProgressError + (*RestoreProgressEntry)(nil), // 7: v1.RestoreProgressEntry + (*RepoStats)(nil), // 8: v1.RepoStats +} +var file_v1_restic_proto_depIdxs = []int32{ + 1, // 0: v1.ResticSnapshot.summary:type_name -> v1.SnapshotSummary + 0, // 1: v1.ResticSnapshotList.snapshots:type_name -> v1.ResticSnapshot + 4, // 2: v1.BackupProgressEntry.status:type_name -> v1.BackupProgressStatusEntry + 5, // 3: v1.BackupProgressEntry.summary:type_name -> v1.BackupProgressSummary + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_v1_restic_proto_init() } +func file_v1_restic_proto_init() { + if File_v1_restic_proto != nil { + return + } + file_v1_restic_proto_msgTypes[3].OneofWrappers = []any{ + (*BackupProgressEntry_Status)(nil), + (*BackupProgressEntry_Summary)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_restic_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_restic_proto_goTypes, + DependencyIndexes: file_v1_restic_proto_depIdxs, + MessageInfos: file_v1_restic_proto_msgTypes, + }.Build() + File_v1_restic_proto = out.File + file_v1_restic_proto_rawDesc = nil + file_v1_restic_proto_goTypes = nil + file_v1_restic_proto_depIdxs = nil +} diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index c04e47c48..2787d29a0 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -1,17 +1,19 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: v1/service.proto package v1 import ( + types "github.com/garethgeorge/backrest/gen/go/types" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" + sync "sync" ) const ( @@ -21,52 +23,1450 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type DoRepoTaskRequest_Task int32 + +const ( + DoRepoTaskRequest_TASK_NONE DoRepoTaskRequest_Task = 0 + DoRepoTaskRequest_TASK_INDEX_SNAPSHOTS DoRepoTaskRequest_Task = 1 + DoRepoTaskRequest_TASK_PRUNE DoRepoTaskRequest_Task = 2 + DoRepoTaskRequest_TASK_CHECK DoRepoTaskRequest_Task = 3 + DoRepoTaskRequest_TASK_STATS DoRepoTaskRequest_Task = 4 + DoRepoTaskRequest_TASK_UNLOCK DoRepoTaskRequest_Task = 5 +) + +// Enum value maps for DoRepoTaskRequest_Task. +var ( + DoRepoTaskRequest_Task_name = map[int32]string{ + 0: "TASK_NONE", + 1: "TASK_INDEX_SNAPSHOTS", + 2: "TASK_PRUNE", + 3: "TASK_CHECK", + 4: "TASK_STATS", + 5: "TASK_UNLOCK", + } + DoRepoTaskRequest_Task_value = map[string]int32{ + "TASK_NONE": 0, + "TASK_INDEX_SNAPSHOTS": 1, + "TASK_PRUNE": 2, + "TASK_CHECK": 3, + "TASK_STATS": 4, + "TASK_UNLOCK": 5, + } +) + +func (x DoRepoTaskRequest_Task) Enum() *DoRepoTaskRequest_Task { + p := new(DoRepoTaskRequest_Task) + *p = x + return p +} + +func (x DoRepoTaskRequest_Task) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DoRepoTaskRequest_Task) Descriptor() protoreflect.EnumDescriptor { + return file_v1_service_proto_enumTypes[0].Descriptor() +} + +func (DoRepoTaskRequest_Task) Type() protoreflect.EnumType { + return &file_v1_service_proto_enumTypes[0] +} + +func (x DoRepoTaskRequest_Task) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DoRepoTaskRequest_Task.Descriptor instead. +func (DoRepoTaskRequest_Task) EnumDescriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{1, 0} +} + +// OpSelector is a message that can be used to select operations e.g. by query. +type OpSelector struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ids []int64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` + InstanceId *string `protobuf:"bytes,6,opt,name=instance_id,json=instanceId,proto3,oneof" json:"instance_id,omitempty"` + RepoGuid *string `protobuf:"bytes,7,opt,name=repo_guid,json=repoGuid,proto3,oneof" json:"repo_guid,omitempty"` + PlanId *string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3,oneof" json:"plan_id,omitempty"` + SnapshotId *string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3,oneof" json:"snapshot_id,omitempty"` + FlowId *int64 `protobuf:"varint,5,opt,name=flow_id,json=flowId,proto3,oneof" json:"flow_id,omitempty"` +} + +func (x *OpSelector) Reset() { + *x = OpSelector{} + mi := &file_v1_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpSelector) ProtoMessage() {} + +func (x *OpSelector) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpSelector.ProtoReflect.Descriptor instead. +func (*OpSelector) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{0} +} + +func (x *OpSelector) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + +func (x *OpSelector) GetInstanceId() string { + if x != nil && x.InstanceId != nil { + return *x.InstanceId + } + return "" +} + +func (x *OpSelector) GetRepoGuid() string { + if x != nil && x.RepoGuid != nil { + return *x.RepoGuid + } + return "" +} + +func (x *OpSelector) GetPlanId() string { + if x != nil && x.PlanId != nil { + return *x.PlanId + } + return "" +} + +func (x *OpSelector) GetSnapshotId() string { + if x != nil && x.SnapshotId != nil { + return *x.SnapshotId + } + return "" +} + +func (x *OpSelector) GetFlowId() int64 { + if x != nil && x.FlowId != nil { + return *x.FlowId + } + return 0 +} + +type DoRepoTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + Task DoRepoTaskRequest_Task `protobuf:"varint,2,opt,name=task,proto3,enum=v1.DoRepoTaskRequest_Task" json:"task,omitempty"` +} + +func (x *DoRepoTaskRequest) Reset() { + *x = DoRepoTaskRequest{} + mi := &file_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DoRepoTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DoRepoTaskRequest) ProtoMessage() {} + +func (x *DoRepoTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DoRepoTaskRequest.ProtoReflect.Descriptor instead. +func (*DoRepoTaskRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *DoRepoTaskRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *DoRepoTaskRequest) GetTask() DoRepoTaskRequest_Task { + if x != nil { + return x.Task + } + return DoRepoTaskRequest_TASK_NONE +} + +type ClearHistoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Selector *OpSelector `protobuf:"bytes,1,opt,name=selector,proto3" json:"selector,omitempty"` + OnlyFailed bool `protobuf:"varint,2,opt,name=only_failed,json=onlyFailed,proto3" json:"only_failed,omitempty"` +} + +func (x *ClearHistoryRequest) Reset() { + *x = ClearHistoryRequest{} + mi := &file_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearHistoryRequest) ProtoMessage() {} + +func (x *ClearHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearHistoryRequest.ProtoReflect.Descriptor instead. +func (*ClearHistoryRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ClearHistoryRequest) GetSelector() *OpSelector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *ClearHistoryRequest) GetOnlyFailed() bool { + if x != nil { + return x.OnlyFailed + } + return false +} + +type ForgetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + SnapshotId string `protobuf:"bytes,3,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` +} + +func (x *ForgetRequest) Reset() { + *x = ForgetRequest{} + mi := &file_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForgetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForgetRequest) ProtoMessage() {} + +func (x *ForgetRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForgetRequest.ProtoReflect.Descriptor instead. +func (*ForgetRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ForgetRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *ForgetRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +func (x *ForgetRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +type ListSnapshotsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` +} + +func (x *ListSnapshotsRequest) Reset() { + *x = ListSnapshotsRequest{} + mi := &file_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotsRequest) ProtoMessage() {} + +func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSnapshotsRequest.ProtoReflect.Descriptor instead. +func (*ListSnapshotsRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ListSnapshotsRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *ListSnapshotsRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +type GetOperationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Selector *OpSelector `protobuf:"bytes,1,opt,name=selector,proto3" json:"selector,omitempty"` + LastN int64 `protobuf:"varint,2,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations +} + +func (x *GetOperationsRequest) Reset() { + *x = GetOperationsRequest{} + mi := &file_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOperationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOperationsRequest) ProtoMessage() {} + +func (x *GetOperationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOperationsRequest.ProtoReflect.Descriptor instead. +func (*GetOperationsRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *GetOperationsRequest) GetSelector() *OpSelector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *GetOperationsRequest) GetLastN() int64 { + if x != nil { + return x.LastN + } + return 0 +} + +type RestoreSnapshotRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PlanId string `protobuf:"bytes,1,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + RepoId string `protobuf:"bytes,5,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + SnapshotId string `protobuf:"bytes,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` +} + +func (x *RestoreSnapshotRequest) Reset() { + *x = RestoreSnapshotRequest{} + mi := &file_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreSnapshotRequest) ProtoMessage() {} + +func (x *RestoreSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreSnapshotRequest.ProtoReflect.Descriptor instead. +func (*RestoreSnapshotRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *RestoreSnapshotRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +func (x *RestoreSnapshotRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *RestoreSnapshotRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +func (x *RestoreSnapshotRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *RestoreSnapshotRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +type ListSnapshotFilesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + SnapshotId string `protobuf:"bytes,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` +} + +func (x *ListSnapshotFilesRequest) Reset() { + *x = ListSnapshotFilesRequest{} + mi := &file_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotFilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotFilesRequest) ProtoMessage() {} + +func (x *ListSnapshotFilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSnapshotFilesRequest.ProtoReflect.Descriptor instead. +func (*ListSnapshotFilesRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *ListSnapshotFilesRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *ListSnapshotFilesRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +func (x *ListSnapshotFilesRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type ListSnapshotFilesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Entries []*LsEntry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"` +} + +func (x *ListSnapshotFilesResponse) Reset() { + *x = ListSnapshotFilesResponse{} + mi := &file_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotFilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotFilesResponse) ProtoMessage() {} + +func (x *ListSnapshotFilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSnapshotFilesResponse.ProtoReflect.Descriptor instead. +func (*ListSnapshotFilesResponse) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *ListSnapshotFilesResponse) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ListSnapshotFilesResponse) GetEntries() []*LsEntry { + if x != nil { + return x.Entries + } + return nil +} + +type LogDataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ref string `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` +} + +func (x *LogDataRequest) Reset() { + *x = LogDataRequest{} + mi := &file_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogDataRequest) ProtoMessage() {} + +func (x *LogDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogDataRequest.ProtoReflect.Descriptor instead. +func (*LogDataRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *LogDataRequest) GetRef() string { + if x != nil { + return x.Ref + } + return "" +} + +type LsEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Uid int64 `protobuf:"varint,4,opt,name=uid,proto3" json:"uid,omitempty"` + Gid int64 `protobuf:"varint,5,opt,name=gid,proto3" json:"gid,omitempty"` + Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` + Mode int64 `protobuf:"varint,7,opt,name=mode,proto3" json:"mode,omitempty"` + Mtime string `protobuf:"bytes,8,opt,name=mtime,proto3" json:"mtime,omitempty"` + Atime string `protobuf:"bytes,9,opt,name=atime,proto3" json:"atime,omitempty"` + Ctime string `protobuf:"bytes,10,opt,name=ctime,proto3" json:"ctime,omitempty"` +} + +func (x *LsEntry) Reset() { + *x = LsEntry{} + mi := &file_v1_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LsEntry) ProtoMessage() {} + +func (x *LsEntry) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LsEntry.ProtoReflect.Descriptor instead. +func (*LsEntry) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{10} +} + +func (x *LsEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LsEntry) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *LsEntry) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *LsEntry) GetUid() int64 { + if x != nil { + return x.Uid + } + return 0 +} + +func (x *LsEntry) GetGid() int64 { + if x != nil { + return x.Gid + } + return 0 +} + +func (x *LsEntry) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *LsEntry) GetMode() int64 { + if x != nil { + return x.Mode + } + return 0 +} + +func (x *LsEntry) GetMtime() string { + if x != nil { + return x.Mtime + } + return "" +} + +func (x *LsEntry) GetAtime() string { + if x != nil { + return x.Atime + } + return "" +} + +func (x *LsEntry) GetCtime() string { + if x != nil { + return x.Ctime + } + return "" +} + +type RunCommandRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` +} + +func (x *RunCommandRequest) Reset() { + *x = RunCommandRequest{} + mi := &file_v1_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunCommandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunCommandRequest) ProtoMessage() {} + +func (x *RunCommandRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunCommandRequest.ProtoReflect.Descriptor instead. +func (*RunCommandRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{11} +} + +func (x *RunCommandRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *RunCommandRequest) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +type SummaryDashboardResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoSummaries []*SummaryDashboardResponse_Summary `protobuf:"bytes,1,rep,name=repo_summaries,json=repoSummaries,proto3" json:"repo_summaries,omitempty"` + PlanSummaries []*SummaryDashboardResponse_Summary `protobuf:"bytes,2,rep,name=plan_summaries,json=planSummaries,proto3" json:"plan_summaries,omitempty"` + ConfigPath string `protobuf:"bytes,10,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` + DataPath string `protobuf:"bytes,11,opt,name=data_path,json=dataPath,proto3" json:"data_path,omitempty"` +} + +func (x *SummaryDashboardResponse) Reset() { + *x = SummaryDashboardResponse{} + mi := &file_v1_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SummaryDashboardResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse) ProtoMessage() {} + +func (x *SummaryDashboardResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12} +} + +func (x *SummaryDashboardResponse) GetRepoSummaries() []*SummaryDashboardResponse_Summary { + if x != nil { + return x.RepoSummaries + } + return nil +} + +func (x *SummaryDashboardResponse) GetPlanSummaries() []*SummaryDashboardResponse_Summary { + if x != nil { + return x.PlanSummaries + } + return nil +} + +func (x *SummaryDashboardResponse) GetConfigPath() string { + if x != nil { + return x.ConfigPath + } + return "" +} + +func (x *SummaryDashboardResponse) GetDataPath() string { + if x != nil { + return x.DataPath + } + return "" +} + +type SummaryDashboardResponse_Summary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + BackupsFailed_30Days int64 `protobuf:"varint,2,opt,name=backups_failed_30days,json=backupsFailed30days,proto3" json:"backups_failed_30days,omitempty"` + BackupsWarningLast_30Days int64 `protobuf:"varint,3,opt,name=backups_warning_last_30days,json=backupsWarningLast30days,proto3" json:"backups_warning_last_30days,omitempty"` + BackupsSuccessLast_30Days int64 `protobuf:"varint,4,opt,name=backups_success_last_30days,json=backupsSuccessLast30days,proto3" json:"backups_success_last_30days,omitempty"` + BytesScannedLast_30Days int64 `protobuf:"varint,5,opt,name=bytes_scanned_last_30days,json=bytesScannedLast30days,proto3" json:"bytes_scanned_last_30days,omitempty"` + BytesAddedLast_30Days int64 `protobuf:"varint,6,opt,name=bytes_added_last_30days,json=bytesAddedLast30days,proto3" json:"bytes_added_last_30days,omitempty"` + TotalSnapshots int64 `protobuf:"varint,7,opt,name=total_snapshots,json=totalSnapshots,proto3" json:"total_snapshots,omitempty"` + BytesScannedAvg int64 `protobuf:"varint,8,opt,name=bytes_scanned_avg,json=bytesScannedAvg,proto3" json:"bytes_scanned_avg,omitempty"` + BytesAddedAvg int64 `protobuf:"varint,9,opt,name=bytes_added_avg,json=bytesAddedAvg,proto3" json:"bytes_added_avg,omitempty"` + NextBackupTimeMs int64 `protobuf:"varint,10,opt,name=next_backup_time_ms,json=nextBackupTimeMs,proto3" json:"next_backup_time_ms,omitempty"` + // Charts + RecentBackups *SummaryDashboardResponse_BackupChart `protobuf:"bytes,11,opt,name=recent_backups,json=recentBackups,proto3" json:"recent_backups,omitempty"` // recent backups +} + +func (x *SummaryDashboardResponse_Summary) Reset() { + *x = SummaryDashboardResponse_Summary{} + mi := &file_v1_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SummaryDashboardResponse_Summary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse_Summary) ProtoMessage() {} + +func (x *SummaryDashboardResponse_Summary) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse_Summary.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse_Summary) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12, 0} +} + +func (x *SummaryDashboardResponse_Summary) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsFailed_30Days() int64 { + if x != nil { + return x.BackupsFailed_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsWarningLast_30Days() int64 { + if x != nil { + return x.BackupsWarningLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsSuccessLast_30Days() int64 { + if x != nil { + return x.BackupsSuccessLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesScannedLast_30Days() int64 { + if x != nil { + return x.BytesScannedLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesAddedLast_30Days() int64 { + if x != nil { + return x.BytesAddedLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetTotalSnapshots() int64 { + if x != nil { + return x.TotalSnapshots + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesScannedAvg() int64 { + if x != nil { + return x.BytesScannedAvg + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesAddedAvg() int64 { + if x != nil { + return x.BytesAddedAvg + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetNextBackupTimeMs() int64 { + if x != nil { + return x.NextBackupTimeMs + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetRecentBackups() *SummaryDashboardResponse_BackupChart { + if x != nil { + return x.RecentBackups + } + return nil +} + +type SummaryDashboardResponse_BackupChart struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FlowId []int64 `protobuf:"varint,1,rep,packed,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + TimestampMs []int64 `protobuf:"varint,2,rep,packed,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + DurationMs []int64 `protobuf:"varint,3,rep,packed,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + Status []OperationStatus `protobuf:"varint,4,rep,packed,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` + BytesAdded []int64 `protobuf:"varint,5,rep,packed,name=bytes_added,json=bytesAdded,proto3" json:"bytes_added,omitempty"` +} + +func (x *SummaryDashboardResponse_BackupChart) Reset() { + *x = SummaryDashboardResponse_BackupChart{} + mi := &file_v1_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SummaryDashboardResponse_BackupChart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse_BackupChart) ProtoMessage() {} + +func (x *SummaryDashboardResponse_BackupChart) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse_BackupChart.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse_BackupChart) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12, 1} +} + +func (x *SummaryDashboardResponse_BackupChart) GetFlowId() []int64 { + if x != nil { + return x.FlowId + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetTimestampMs() []int64 { + if x != nil { + return x.TimestampMs + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetDurationMs() []int64 { + if x != nil { + return x.DurationMs + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetStatus() []OperationStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetBytesAdded() []int64 { + if x != nil { + return x.BytesAdded + } + return nil +} + var File_v1_service_proto protoreflect.FileDescriptor var file_v1_service_proto_rawDesc = []byte{ 0x0a, 0x10, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x32, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, - 0x12, 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x74, + 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8e, 0x02, 0x0a, 0x0a, + 0x4f, 0x70, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0b, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x88, + 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x67, 0x75, 0x69, 0x64, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6f, 0x47, 0x75, 0x69, + 0x64, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x88, + 0x01, 0x01, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, + 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x48, 0x04, 0x52, 0x06, 0x66, 0x6c, 0x6f, + 0x77, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x5f, + 0x67, 0x75, 0x69, 0x64, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, + 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, + 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x22, 0xce, 0x01, 0x0a, + 0x11, 0x44, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, + 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x22, 0x70, 0x0a, 0x04, 0x54, + 0x61, 0x73, 0x6b, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, + 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, + 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, + 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, + 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, + 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x04, 0x12, 0x0f, 0x0a, 0x0b, + 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x55, 0x4e, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x05, 0x22, 0x62, 0x0a, + 0x13, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6f, 0x6e, 0x6c, 0x79, 0x46, 0x61, 0x69, 0x6c, 0x65, + 0x64, 0x22, 0x62, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, + 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, + 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, + 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, + 0x59, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x97, 0x01, 0x0a, 0x16, 0x52, + 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, + 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x56, + 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, + 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, + 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x65, 0x66, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, + 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, + 0x22, 0x46, 0x0a, 0x11, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0xea, 0x07, 0x0a, 0x18, 0x53, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x73, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, + 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x52, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x69, + 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12, + 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x61, 0x74, 0x68, + 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x50, 0x61, 0x74, 0x68, 0x1a, 0xba, 0x04, + 0x0a, 0x07, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x62, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x73, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x33, 0x30, 0x64, 0x61, + 0x79, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x3d, 0x0a, + 0x1b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x5f, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, + 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x18, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x57, 0x61, 0x72, 0x6e, 0x69, + 0x6e, 0x67, 0x4c, 0x61, 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x3d, 0x0a, 0x1b, + 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x18, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x4c, 0x61, 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x4c, 0x61, 0x73, 0x74, + 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x62, 0x79, 0x74, 0x65, 0x73, 0x41, 0x64, + 0x64, 0x65, 0x64, 0x4c, 0x61, 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x27, 0x0a, + 0x0f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x61, 0x76, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x41, + 0x76, 0x67, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x64, 0x64, 0x65, + 0x64, 0x5f, 0x61, 0x76, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x41, 0x64, 0x64, 0x65, 0x64, 0x41, 0x76, 0x67, 0x12, 0x2d, 0x0a, 0x13, 0x6e, 0x65, + 0x78, 0x74, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x72, 0x65, 0x63, + 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x28, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, + 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x43, 0x68, 0x61, 0x72, 0x74, 0x52, 0x0d, 0x72, 0x65, 0x63, + 0x65, 0x6e, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x1a, 0xb8, 0x01, 0x0a, 0x0b, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x43, 0x68, 0x61, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, 0x66, 0x6c, 0x6f, + 0x77, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x4d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x64, + 0x64, 0x65, 0x64, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x41, 0x64, 0x64, 0x65, 0x64, 0x32, 0xa7, 0x09, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, + 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x25, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x44, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, - 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var file_v1_service_proto_goTypes = []interface{}{ - (*emptypb.Empty)(nil), // 0: google.protobuf.Empty - (*Config)(nil), // 1: v1.Config - (*Event)(nil), // 2: v1.Event + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x0f, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x70, 0x6f, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, + 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x10, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x21, 0x0a, + 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, + 0x12, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x12, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, + 0x12, 0x44, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x11, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, + 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x36, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0a, 0x44, 0x6f, 0x52, 0x65, 0x70, + 0x6f, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x52, 0x65, 0x70, + 0x6f, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, + 0x12, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3f, 0x0a, + 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, + 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, + 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x52, + 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, + 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, + 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, + 0x12, 0x4d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, + 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, + 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, + 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, + 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_service_proto_rawDescOnce sync.Once + file_v1_service_proto_rawDescData = file_v1_service_proto_rawDesc +) + +func file_v1_service_proto_rawDescGZIP() []byte { + file_v1_service_proto_rawDescOnce.Do(func() { + file_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_service_proto_rawDescData) + }) + return file_v1_service_proto_rawDescData +} + +var file_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_v1_service_proto_goTypes = []any{ + (DoRepoTaskRequest_Task)(0), // 0: v1.DoRepoTaskRequest.Task + (*OpSelector)(nil), // 1: v1.OpSelector + (*DoRepoTaskRequest)(nil), // 2: v1.DoRepoTaskRequest + (*ClearHistoryRequest)(nil), // 3: v1.ClearHistoryRequest + (*ForgetRequest)(nil), // 4: v1.ForgetRequest + (*ListSnapshotsRequest)(nil), // 5: v1.ListSnapshotsRequest + (*GetOperationsRequest)(nil), // 6: v1.GetOperationsRequest + (*RestoreSnapshotRequest)(nil), // 7: v1.RestoreSnapshotRequest + (*ListSnapshotFilesRequest)(nil), // 8: v1.ListSnapshotFilesRequest + (*ListSnapshotFilesResponse)(nil), // 9: v1.ListSnapshotFilesResponse + (*LogDataRequest)(nil), // 10: v1.LogDataRequest + (*LsEntry)(nil), // 11: v1.LsEntry + (*RunCommandRequest)(nil), // 12: v1.RunCommandRequest + (*SummaryDashboardResponse)(nil), // 13: v1.SummaryDashboardResponse + (*SummaryDashboardResponse_Summary)(nil), // 14: v1.SummaryDashboardResponse.Summary + (*SummaryDashboardResponse_BackupChart)(nil), // 15: v1.SummaryDashboardResponse.BackupChart + (OperationStatus)(0), // 16: v1.OperationStatus + (*emptypb.Empty)(nil), // 17: google.protobuf.Empty + (*Config)(nil), // 18: v1.Config + (*Repo)(nil), // 19: v1.Repo + (*types.StringValue)(nil), // 20: types.StringValue + (*types.Int64Value)(nil), // 21: types.Int64Value + (*types.BoolValue)(nil), // 22: types.BoolValue + (*OperationEvent)(nil), // 23: v1.OperationEvent + (*OperationList)(nil), // 24: v1.OperationList + (*ResticSnapshotList)(nil), // 25: v1.ResticSnapshotList + (*types.BytesValue)(nil), // 26: types.BytesValue + (*types.StringList)(nil), // 27: types.StringList } var file_v1_service_proto_depIdxs = []int32{ - 0, // 0: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty - 1, // 1: v1.ResticUI.SetConfig:input_type -> v1.Config - 0, // 2: v1.ResticUI.GetEvents:input_type -> google.protobuf.Empty - 1, // 3: v1.ResticUI.GetConfig:output_type -> v1.Config - 1, // 4: v1.ResticUI.SetConfig:output_type -> v1.Config - 2, // 5: v1.ResticUI.GetEvents:output_type -> v1.Event - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 0, // 0: v1.DoRepoTaskRequest.task:type_name -> v1.DoRepoTaskRequest.Task + 1, // 1: v1.ClearHistoryRequest.selector:type_name -> v1.OpSelector + 1, // 2: v1.GetOperationsRequest.selector:type_name -> v1.OpSelector + 11, // 3: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry + 14, // 4: v1.SummaryDashboardResponse.repo_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 14, // 5: v1.SummaryDashboardResponse.plan_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 15, // 6: v1.SummaryDashboardResponse.Summary.recent_backups:type_name -> v1.SummaryDashboardResponse.BackupChart + 16, // 7: v1.SummaryDashboardResponse.BackupChart.status:type_name -> v1.OperationStatus + 17, // 8: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty + 18, // 9: v1.Backrest.SetConfig:input_type -> v1.Config + 19, // 10: v1.Backrest.CheckRepoExists:input_type -> v1.Repo + 19, // 11: v1.Backrest.AddRepo:input_type -> v1.Repo + 20, // 12: v1.Backrest.RemoveRepo:input_type -> types.StringValue + 17, // 13: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty + 6, // 14: v1.Backrest.GetOperations:input_type -> v1.GetOperationsRequest + 5, // 15: v1.Backrest.ListSnapshots:input_type -> v1.ListSnapshotsRequest + 8, // 16: v1.Backrest.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest + 20, // 17: v1.Backrest.Backup:input_type -> types.StringValue + 2, // 18: v1.Backrest.DoRepoTask:input_type -> v1.DoRepoTaskRequest + 4, // 19: v1.Backrest.Forget:input_type -> v1.ForgetRequest + 7, // 20: v1.Backrest.Restore:input_type -> v1.RestoreSnapshotRequest + 21, // 21: v1.Backrest.Cancel:input_type -> types.Int64Value + 10, // 22: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest + 12, // 23: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest + 21, // 24: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value + 3, // 25: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest + 20, // 26: v1.Backrest.PathAutocomplete:input_type -> types.StringValue + 17, // 27: v1.Backrest.GetSummaryDashboard:input_type -> google.protobuf.Empty + 18, // 28: v1.Backrest.GetConfig:output_type -> v1.Config + 18, // 29: v1.Backrest.SetConfig:output_type -> v1.Config + 22, // 30: v1.Backrest.CheckRepoExists:output_type -> types.BoolValue + 18, // 31: v1.Backrest.AddRepo:output_type -> v1.Config + 18, // 32: v1.Backrest.RemoveRepo:output_type -> v1.Config + 23, // 33: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent + 24, // 34: v1.Backrest.GetOperations:output_type -> v1.OperationList + 25, // 35: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList + 9, // 36: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse + 17, // 37: v1.Backrest.Backup:output_type -> google.protobuf.Empty + 17, // 38: v1.Backrest.DoRepoTask:output_type -> google.protobuf.Empty + 17, // 39: v1.Backrest.Forget:output_type -> google.protobuf.Empty + 17, // 40: v1.Backrest.Restore:output_type -> google.protobuf.Empty + 17, // 41: v1.Backrest.Cancel:output_type -> google.protobuf.Empty + 26, // 42: v1.Backrest.GetLogs:output_type -> types.BytesValue + 21, // 43: v1.Backrest.RunCommand:output_type -> types.Int64Value + 20, // 44: v1.Backrest.GetDownloadURL:output_type -> types.StringValue + 17, // 45: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty + 27, // 46: v1.Backrest.PathAutocomplete:output_type -> types.StringList + 13, // 47: v1.Backrest.GetSummaryDashboard:output_type -> v1.SummaryDashboardResponse + 28, // [28:48] is the sub-list for method output_type + 8, // [8:28] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_v1_service_proto_init() } @@ -75,19 +1475,23 @@ func file_v1_service_proto_init() { return } file_v1_config_proto_init() - file_v1_events_proto_init() + file_v1_restic_proto_init() + file_v1_operations_proto_init() + file_v1_service_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_service_proto_rawDesc, - NumEnums: 0, - NumMessages: 0, + NumEnums: 1, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, GoTypes: file_v1_service_proto_goTypes, DependencyIndexes: file_v1_service_proto_depIdxs, + EnumInfos: file_v1_service_proto_enumTypes, + MessageInfos: file_v1_service_proto_msgTypes, }.Build() File_v1_service_proto = out.File file_v1_service_proto_rawDesc = nil diff --git a/gen/go/v1/service.pb.gw.go b/gen/go/v1/service.pb.gw.go deleted file mode 100644 index a2cd9597c..000000000 --- a/gen/go/v1/service.pb.gw.go +++ /dev/null @@ -1,291 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: v1/service.proto - -/* -Package v1 is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package v1 - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/emptypb" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -func request_ResticUI_GetConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq emptypb.Empty - var metadata runtime.ServerMetadata - - msg, err := client.GetConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_ResticUI_GetConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq emptypb.Empty - var metadata runtime.ServerMetadata - - msg, err := server.GetConfig(ctx, &protoReq) - return msg, metadata, err - -} - -func request_ResticUI_SetConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Config - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.SetConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_ResticUI_SetConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Config - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.SetConfig(ctx, &protoReq) - return msg, metadata, err - -} - -func request_ResticUI_GetEvents_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (ResticUI_GetEventsClient, runtime.ServerMetadata, error) { - var protoReq emptypb.Empty - var metadata runtime.ServerMetadata - - stream, err := client.GetEvents(ctx, &protoReq) - if err != nil { - return nil, metadata, err - } - header, err := stream.Header() - if err != nil { - return nil, metadata, err - } - metadata.HeaderMD = header - return stream, metadata, nil - -} - -// RegisterResticUIHandlerServer registers the http handlers for service ResticUI to "mux". -// UnaryRPC :call ResticUIServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterResticUIHandlerFromEndpoint instead. -func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ResticUIServer) error { - - mux.Handle("GET", pattern_ResticUI_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/GetConfig", runtime.WithHTTPPathPattern("/v1/config")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_ResticUI_GetConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ResticUI_GetConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_ResticUI_SetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/SetConfig", runtime.WithHTTPPathPattern("/v1/config")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_ResticUI_SetConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ResticUI_SetConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("GET", pattern_ResticUI_GetEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") - _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - }) - - return nil -} - -// RegisterResticUIHandlerFromEndpoint is same as RegisterResticUIHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterResticUIHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterResticUIHandler(ctx, mux, conn) -} - -// RegisterResticUIHandler registers the http handlers for service ResticUI to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterResticUIHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterResticUIHandlerClient(ctx, mux, NewResticUIClient(conn)) -} - -// RegisterResticUIHandlerClient registers the http handlers for service ResticUI -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ResticUIClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ResticUIClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "ResticUIClient" to call the correct interceptors. -func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ResticUIClient) error { - - mux.Handle("GET", pattern_ResticUI_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/GetConfig", runtime.WithHTTPPathPattern("/v1/config")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_ResticUI_GetConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ResticUI_GetConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_ResticUI_SetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/SetConfig", runtime.WithHTTPPathPattern("/v1/config")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_ResticUI_SetConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ResticUI_SetConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("GET", pattern_ResticUI_GetEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/GetEvents", runtime.WithHTTPPathPattern("/v1/events")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_ResticUI_GetEvents_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ResticUI_GetEvents_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_ResticUI_GetConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "config"}, "")) - - pattern_ResticUI_SetConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "config"}, "")) - - pattern_ResticUI_GetEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "events"}, "")) -) - -var ( - forward_ResticUI_GetConfig_0 = runtime.ForwardResponseMessage - - forward_ResticUI_SetConfig_0 = runtime.ForwardResponseMessage - - forward_ResticUI_GetEvents_0 = runtime.ForwardResponseStream -) diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 99c81a176..1e3306e7c 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 +// - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: v1/service.proto @@ -8,6 +8,7 @@ package v1 import ( context "context" + types "github.com/garethgeorge/backrest/gen/go/types" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -16,56 +17,134 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( - ResticUI_GetConfig_FullMethodName = "/v1.ResticUI/GetConfig" - ResticUI_SetConfig_FullMethodName = "/v1.ResticUI/SetConfig" - ResticUI_GetEvents_FullMethodName = "/v1.ResticUI/GetEvents" + Backrest_GetConfig_FullMethodName = "/v1.Backrest/GetConfig" + Backrest_SetConfig_FullMethodName = "/v1.Backrest/SetConfig" + Backrest_CheckRepoExists_FullMethodName = "/v1.Backrest/CheckRepoExists" + Backrest_AddRepo_FullMethodName = "/v1.Backrest/AddRepo" + Backrest_RemoveRepo_FullMethodName = "/v1.Backrest/RemoveRepo" + Backrest_GetOperationEvents_FullMethodName = "/v1.Backrest/GetOperationEvents" + Backrest_GetOperations_FullMethodName = "/v1.Backrest/GetOperations" + Backrest_ListSnapshots_FullMethodName = "/v1.Backrest/ListSnapshots" + Backrest_ListSnapshotFiles_FullMethodName = "/v1.Backrest/ListSnapshotFiles" + Backrest_Backup_FullMethodName = "/v1.Backrest/Backup" + Backrest_DoRepoTask_FullMethodName = "/v1.Backrest/DoRepoTask" + Backrest_Forget_FullMethodName = "/v1.Backrest/Forget" + Backrest_Restore_FullMethodName = "/v1.Backrest/Restore" + Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel" + Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs" + Backrest_RunCommand_FullMethodName = "/v1.Backrest/RunCommand" + Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL" + Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory" + Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete" + Backrest_GetSummaryDashboard_FullMethodName = "/v1.Backrest/GetSummaryDashboard" ) -// ResticUIClient is the client API for ResticUI service. +// BackrestClient is the client API for Backrest service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type ResticUIClient interface { +type BackrestClient interface { GetConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) SetConfig(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Config, error) - GetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ResticUI_GetEventsClient, error) + CheckRepoExists(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*types.BoolValue, error) + AddRepo(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*Config, error) + RemoveRepo(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*Config, error) + GetOperationEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OperationEvent], error) + GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*OperationList, error) + ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ResticSnapshotList, error) + ListSnapshotFiles(ctx context.Context, in *ListSnapshotFilesRequest, opts ...grpc.CallOption) (*ListSnapshotFilesResponse, error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) + // DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + DoRepoTask(ctx context.Context, in *DoRepoTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + Forget(ctx context.Context, in *ForgetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Restore schedules a restore operation. + Restore(ctx context.Context, in *RestoreSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error) + // GetLogs returns the keyed large data for the given operation. + GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[types.BytesValue], error) + // RunCommand executes a generic restic command on the repository. + RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (*types.Int64Value, error) + // GetDownloadURL returns a signed download URL given a forget operation ID. + GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) + // Clears the history of operations + ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. + PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryDashboardResponse, error) } -type resticUIClient struct { +type backrestClient struct { cc grpc.ClientConnInterface } -func NewResticUIClient(cc grpc.ClientConnInterface) ResticUIClient { - return &resticUIClient{cc} +func NewBackrestClient(cc grpc.ClientConnInterface) BackrestClient { + return &backrestClient{cc} } -func (c *resticUIClient) GetConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) { +func (c *backrestClient) GetConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Config) - err := c.cc.Invoke(ctx, ResticUI_GetConfig_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, Backrest_GetConfig_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *resticUIClient) SetConfig(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Config, error) { +func (c *backrestClient) SetConfig(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Config) - err := c.cc.Invoke(ctx, ResticUI_SetConfig_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, Backrest_SetConfig_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *resticUIClient) GetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ResticUI_GetEventsClient, error) { - stream, err := c.cc.NewStream(ctx, &ResticUI_ServiceDesc.Streams[0], ResticUI_GetEvents_FullMethodName, opts...) +func (c *backrestClient) CheckRepoExists(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*types.BoolValue, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.BoolValue) + err := c.cc.Invoke(ctx, Backrest_CheckRepoExists_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } - x := &resticUIGetEventsClient{stream} + return out, nil +} + +func (c *backrestClient) AddRepo(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Config) + err := c.cc.Invoke(ctx, Backrest_AddRepo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) RemoveRepo(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Config) + err := c.cc.Invoke(ctx, Backrest_RemoveRepo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) GetOperationEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OperationEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[0], Backrest_GetOperationEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, OperationEvent]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -75,136 +154,719 @@ func (c *resticUIClient) GetEvents(ctx context.Context, in *emptypb.Empty, opts return x, nil } -type ResticUI_GetEventsClient interface { - Recv() (*Event, error) - grpc.ClientStream +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Backrest_GetOperationEventsClient = grpc.ServerStreamingClient[OperationEvent] + +func (c *backrestClient) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*OperationList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OperationList) + err := c.cc.Invoke(ctx, Backrest_GetOperations_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil } -type resticUIGetEventsClient struct { - grpc.ClientStream +func (c *backrestClient) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ResticSnapshotList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResticSnapshotList) + err := c.cc.Invoke(ctx, Backrest_ListSnapshots_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil } -func (x *resticUIGetEventsClient) Recv() (*Event, error) { - m := new(Event) - if err := x.ClientStream.RecvMsg(m); err != nil { +func (c *backrestClient) ListSnapshotFiles(ctx context.Context, in *ListSnapshotFilesRequest, opts ...grpc.CallOption) (*ListSnapshotFilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSnapshotFilesResponse) + err := c.cc.Invoke(ctx, Backrest_ListSnapshotFiles_FullMethodName, in, out, cOpts...) + if err != nil { return nil, err } - return m, nil + return out, nil } -// ResticUIServer is the server API for ResticUI service. -// All implementations must embed UnimplementedResticUIServer -// for forward compatibility -type ResticUIServer interface { +func (c *backrestClient) Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_Backup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) DoRepoTask(ctx context.Context, in *DoRepoTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_DoRepoTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) Forget(ctx context.Context, in *ForgetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_Forget_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) Restore(ctx context.Context, in *RestoreSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_Restore_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_Cancel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[types.BytesValue], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[1], Backrest_GetLogs_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[LogDataRequest, types.BytesValue]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Backrest_GetLogsClient = grpc.ServerStreamingClient[types.BytesValue] + +func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (*types.Int64Value, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.Int64Value) + err := c.cc.Invoke(ctx, Backrest_RunCommand_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.StringValue) + err := c.cc.Invoke(ctx, Backrest_GetDownloadURL_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Backrest_ClearHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.StringList) + err := c.cc.Invoke(ctx, Backrest_PathAutocomplete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *backrestClient) GetSummaryDashboard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryDashboardResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SummaryDashboardResponse) + err := c.cc.Invoke(ctx, Backrest_GetSummaryDashboard_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// BackrestServer is the server API for Backrest service. +// All implementations must embed UnimplementedBackrestServer +// for forward compatibility. +type BackrestServer interface { GetConfig(context.Context, *emptypb.Empty) (*Config, error) SetConfig(context.Context, *Config) (*Config, error) - GetEvents(*emptypb.Empty, ResticUI_GetEventsServer) error - mustEmbedUnimplementedResticUIServer() + CheckRepoExists(context.Context, *Repo) (*types.BoolValue, error) + AddRepo(context.Context, *Repo) (*Config, error) + RemoveRepo(context.Context, *types.StringValue) (*Config, error) + GetOperationEvents(*emptypb.Empty, grpc.ServerStreamingServer[OperationEvent]) error + GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) + ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) + ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) + // DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + DoRepoTask(context.Context, *DoRepoTaskRequest) (*emptypb.Empty, error) + // Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + Forget(context.Context, *ForgetRequest) (*emptypb.Empty, error) + // Restore schedules a restore operation. + Restore(context.Context, *RestoreSnapshotRequest) (*emptypb.Empty, error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) + // GetLogs returns the keyed large data for the given operation. + GetLogs(*LogDataRequest, grpc.ServerStreamingServer[types.BytesValue]) error + // RunCommand executes a generic restic command on the repository. + RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) + // GetDownloadURL returns a signed download URL given a forget operation ID. + GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) + // Clears the history of operations + ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. + PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) + mustEmbedUnimplementedBackrestServer() } -// UnimplementedResticUIServer must be embedded to have forward compatible implementations. -type UnimplementedResticUIServer struct { -} +// UnimplementedBackrestServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedBackrestServer struct{} -func (UnimplementedResticUIServer) GetConfig(context.Context, *emptypb.Empty) (*Config, error) { +func (UnimplementedBackrestServer) GetConfig(context.Context, *emptypb.Empty) (*Config, error) { return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") } -func (UnimplementedResticUIServer) SetConfig(context.Context, *Config) (*Config, error) { +func (UnimplementedBackrestServer) SetConfig(context.Context, *Config) (*Config, error) { return nil, status.Errorf(codes.Unimplemented, "method SetConfig not implemented") } -func (UnimplementedResticUIServer) GetEvents(*emptypb.Empty, ResticUI_GetEventsServer) error { - return status.Errorf(codes.Unimplemented, "method GetEvents not implemented") +func (UnimplementedBackrestServer) CheckRepoExists(context.Context, *Repo) (*types.BoolValue, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckRepoExists not implemented") +} +func (UnimplementedBackrestServer) AddRepo(context.Context, *Repo) (*Config, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddRepo not implemented") +} +func (UnimplementedBackrestServer) RemoveRepo(context.Context, *types.StringValue) (*Config, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveRepo not implemented") +} +func (UnimplementedBackrestServer) GetOperationEvents(*emptypb.Empty, grpc.ServerStreamingServer[OperationEvent]) error { + return status.Errorf(codes.Unimplemented, "method GetOperationEvents not implemented") +} +func (UnimplementedBackrestServer) GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") +} +func (UnimplementedBackrestServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented") +} +func (UnimplementedBackrestServer) ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListSnapshotFiles not implemented") +} +func (UnimplementedBackrestServer) Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented") +} +func (UnimplementedBackrestServer) DoRepoTask(context.Context, *DoRepoTaskRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoRepoTask not implemented") +} +func (UnimplementedBackrestServer) Forget(context.Context, *ForgetRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Forget not implemented") +} +func (UnimplementedBackrestServer) Restore(context.Context, *RestoreSnapshotRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented") } -func (UnimplementedResticUIServer) mustEmbedUnimplementedResticUIServer() {} +func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented") +} +func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, grpc.ServerStreamingServer[types.BytesValue]) error { + return status.Errorf(codes.Unimplemented, "method GetLogs not implemented") +} +func (UnimplementedBackrestServer) RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) { + return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented") +} +func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented") +} +func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented") +} +func (UnimplementedBackrestServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { + return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") +} +func (UnimplementedBackrestServer) GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSummaryDashboard not implemented") +} +func (UnimplementedBackrestServer) mustEmbedUnimplementedBackrestServer() {} +func (UnimplementedBackrestServer) testEmbeddedByValue() {} -// UnsafeResticUIServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ResticUIServer will +// UnsafeBackrestServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to BackrestServer will // result in compilation errors. -type UnsafeResticUIServer interface { - mustEmbedUnimplementedResticUIServer() +type UnsafeBackrestServer interface { + mustEmbedUnimplementedBackrestServer() } -func RegisterResticUIServer(s grpc.ServiceRegistrar, srv ResticUIServer) { - s.RegisterService(&ResticUI_ServiceDesc, srv) +func RegisterBackrestServer(s grpc.ServiceRegistrar, srv BackrestServer) { + // If the following call pancis, it indicates UnimplementedBackrestServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Backrest_ServiceDesc, srv) } -func _ResticUI_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _Backrest_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ResticUIServer).GetConfig(ctx, in) + return srv.(BackrestServer).GetConfig(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ResticUI_GetConfig_FullMethodName, + FullMethod: Backrest_GetConfig_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResticUIServer).GetConfig(ctx, req.(*emptypb.Empty)) + return srv.(BackrestServer).GetConfig(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } -func _ResticUI_SetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _Backrest_SetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Config) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ResticUIServer).SetConfig(ctx, in) + return srv.(BackrestServer).SetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_SetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).SetConfig(ctx, req.(*Config)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_CheckRepoExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Repo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).CheckRepoExists(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_CheckRepoExists_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).CheckRepoExists(ctx, req.(*Repo)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_AddRepo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Repo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).AddRepo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_AddRepo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).AddRepo(ctx, req.(*Repo)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_RemoveRepo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).RemoveRepo(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ResticUI_SetConfig_FullMethodName, + FullMethod: Backrest_RemoveRepo_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResticUIServer).SetConfig(ctx, req.(*Config)) + return srv.(BackrestServer).RemoveRepo(ctx, req.(*types.StringValue)) } return interceptor(ctx, in, info, handler) } -func _ResticUI_GetEvents_Handler(srv interface{}, stream grpc.ServerStream) error { +func _Backrest_GetOperationEvents_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(ResticUIServer).GetEvents(m, &resticUIGetEventsServer{stream}) + return srv.(BackrestServer).GetOperationEvents(m, &grpc.GenericServerStream[emptypb.Empty, OperationEvent]{ServerStream: stream}) } -type ResticUI_GetEventsServer interface { - Send(*Event) error - grpc.ServerStream +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Backrest_GetOperationEventsServer = grpc.ServerStreamingServer[OperationEvent] + +func _Backrest_GetOperations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOperationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).GetOperations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_GetOperations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).GetOperations(ctx, req.(*GetOperationsRequest)) + } + return interceptor(ctx, in, info, handler) } -type resticUIGetEventsServer struct { - grpc.ServerStream +func _Backrest_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSnapshotsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).ListSnapshots(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_ListSnapshots_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).ListSnapshots(ctx, req.(*ListSnapshotsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_ListSnapshotFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSnapshotFilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).ListSnapshotFiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_ListSnapshotFiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).ListSnapshotFiles(ctx, req.(*ListSnapshotFilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_Backup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).Backup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_Backup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).Backup(ctx, req.(*types.StringValue)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_DoRepoTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DoRepoTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).DoRepoTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_DoRepoTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).DoRepoTask(ctx, req.(*DoRepoTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_Forget_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ForgetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).Forget(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_Forget_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).Forget(ctx, req.(*ForgetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_Restore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestoreSnapshotRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).Restore(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_Restore_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).Restore(ctx, req.(*RestoreSnapshotRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_Cancel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.Int64Value) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).Cancel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_Cancel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).Cancel(ctx, req.(*types.Int64Value)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_GetLogs_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(LogDataRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(BackrestServer).GetLogs(m, &grpc.GenericServerStream[LogDataRequest, types.BytesValue]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Backrest_GetLogsServer = grpc.ServerStreamingServer[types.BytesValue] + +func _Backrest_RunCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunCommandRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).RunCommand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_RunCommand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).RunCommand(ctx, req.(*RunCommandRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.Int64Value) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).GetDownloadURL(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_GetDownloadURL_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).GetDownloadURL(ctx, req.(*types.Int64Value)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_ClearHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClearHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).ClearHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_ClearHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).ClearHistory(ctx, req.(*ClearHistoryRequest)) + } + return interceptor(ctx, in, info, handler) } -func (x *resticUIGetEventsServer) Send(m *Event) error { - return x.ServerStream.SendMsg(m) +func _Backrest_PathAutocomplete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).PathAutocomplete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_PathAutocomplete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).PathAutocomplete(ctx, req.(*types.StringValue)) + } + return interceptor(ctx, in, info, handler) +} + +func _Backrest_GetSummaryDashboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).GetSummaryDashboard(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_GetSummaryDashboard_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).GetSummaryDashboard(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) } -// ResticUI_ServiceDesc is the grpc.ServiceDesc for ResticUI service. +// Backrest_ServiceDesc is the grpc.ServiceDesc for Backrest service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var ResticUI_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "v1.ResticUI", - HandlerType: (*ResticUIServer)(nil), +var Backrest_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "v1.Backrest", + HandlerType: (*BackrestServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetConfig", - Handler: _ResticUI_GetConfig_Handler, + Handler: _Backrest_GetConfig_Handler, }, { MethodName: "SetConfig", - Handler: _ResticUI_SetConfig_Handler, + Handler: _Backrest_SetConfig_Handler, + }, + { + MethodName: "CheckRepoExists", + Handler: _Backrest_CheckRepoExists_Handler, + }, + { + MethodName: "AddRepo", + Handler: _Backrest_AddRepo_Handler, + }, + { + MethodName: "RemoveRepo", + Handler: _Backrest_RemoveRepo_Handler, + }, + { + MethodName: "GetOperations", + Handler: _Backrest_GetOperations_Handler, + }, + { + MethodName: "ListSnapshots", + Handler: _Backrest_ListSnapshots_Handler, + }, + { + MethodName: "ListSnapshotFiles", + Handler: _Backrest_ListSnapshotFiles_Handler, + }, + { + MethodName: "Backup", + Handler: _Backrest_Backup_Handler, + }, + { + MethodName: "DoRepoTask", + Handler: _Backrest_DoRepoTask_Handler, + }, + { + MethodName: "Forget", + Handler: _Backrest_Forget_Handler, + }, + { + MethodName: "Restore", + Handler: _Backrest_Restore_Handler, + }, + { + MethodName: "Cancel", + Handler: _Backrest_Cancel_Handler, + }, + { + MethodName: "RunCommand", + Handler: _Backrest_RunCommand_Handler, + }, + { + MethodName: "GetDownloadURL", + Handler: _Backrest_GetDownloadURL_Handler, + }, + { + MethodName: "ClearHistory", + Handler: _Backrest_ClearHistory_Handler, + }, + { + MethodName: "PathAutocomplete", + Handler: _Backrest_PathAutocomplete_Handler, + }, + { + MethodName: "GetSummaryDashboard", + Handler: _Backrest_GetSummaryDashboard_Handler, }, }, Streams: []grpc.StreamDesc{ { - StreamName: "GetEvents", - Handler: _ResticUI_GetEvents_Handler, + StreamName: "GetOperationEvents", + Handler: _Backrest_GetOperationEvents_Handler, + ServerStreams: true, + }, + { + StreamName: "GetLogs", + Handler: _Backrest_GetLogs_Handler, ServerStreams: true, }, }, diff --git a/gen/go/v1/syncservice.pb.go b/gen/go/v1/syncservice.pb.go new file mode 100644 index 000000000..71c8367da --- /dev/null +++ b/gen/go/v1/syncservice.pb.go @@ -0,0 +1,1140 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc (unknown) +// source: v1/syncservice.proto + +package v1 + +import ( + _ "github.com/garethgeorge/backrest/gen/go/types" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SyncConnectionState int32 + +const ( + SyncConnectionState_CONNECTION_STATE_UNKNOWN SyncConnectionState = 0 + SyncConnectionState_CONNECTION_STATE_PENDING SyncConnectionState = 1 + SyncConnectionState_CONNECTION_STATE_CONNECTED SyncConnectionState = 2 + SyncConnectionState_CONNECTION_STATE_DISCONNECTED SyncConnectionState = 3 + SyncConnectionState_CONNECTION_STATE_RETRY_WAIT SyncConnectionState = 4 + SyncConnectionState_CONNECTION_STATE_ERROR_AUTH SyncConnectionState = 10 + SyncConnectionState_CONNECTION_STATE_ERROR_PROTOCOL SyncConnectionState = 11 +) + +// Enum value maps for SyncConnectionState. +var ( + SyncConnectionState_name = map[int32]string{ + 0: "CONNECTION_STATE_UNKNOWN", + 1: "CONNECTION_STATE_PENDING", + 2: "CONNECTION_STATE_CONNECTED", + 3: "CONNECTION_STATE_DISCONNECTED", + 4: "CONNECTION_STATE_RETRY_WAIT", + 10: "CONNECTION_STATE_ERROR_AUTH", + 11: "CONNECTION_STATE_ERROR_PROTOCOL", + } + SyncConnectionState_value = map[string]int32{ + "CONNECTION_STATE_UNKNOWN": 0, + "CONNECTION_STATE_PENDING": 1, + "CONNECTION_STATE_CONNECTED": 2, + "CONNECTION_STATE_DISCONNECTED": 3, + "CONNECTION_STATE_RETRY_WAIT": 4, + "CONNECTION_STATE_ERROR_AUTH": 10, + "CONNECTION_STATE_ERROR_PROTOCOL": 11, + } +) + +func (x SyncConnectionState) Enum() *SyncConnectionState { + p := new(SyncConnectionState) + *p = x + return p +} + +func (x SyncConnectionState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SyncConnectionState) Descriptor() protoreflect.EnumDescriptor { + return file_v1_syncservice_proto_enumTypes[0].Descriptor() +} + +func (SyncConnectionState) Type() protoreflect.EnumType { + return &file_v1_syncservice_proto_enumTypes[0] +} + +func (x SyncConnectionState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SyncConnectionState.Descriptor instead. +func (SyncConnectionState) EnumDescriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{0} +} + +type SyncStreamItem_RepoConnectionState int32 + +const ( + SyncStreamItem_CONNECTION_STATE_UNKNOWN SyncStreamItem_RepoConnectionState = 0 + SyncStreamItem_CONNECTION_STATE_PENDING SyncStreamItem_RepoConnectionState = 1 // queried, response not yet received. + SyncStreamItem_CONNECTION_STATE_CONNECTED SyncStreamItem_RepoConnectionState = 2 + SyncStreamItem_CONNECTION_STATE_UNAUTHORIZED SyncStreamItem_RepoConnectionState = 3 + SyncStreamItem_CONNECTION_STATE_NOT_FOUND SyncStreamItem_RepoConnectionState = 4 +) + +// Enum value maps for SyncStreamItem_RepoConnectionState. +var ( + SyncStreamItem_RepoConnectionState_name = map[int32]string{ + 0: "CONNECTION_STATE_UNKNOWN", + 1: "CONNECTION_STATE_PENDING", + 2: "CONNECTION_STATE_CONNECTED", + 3: "CONNECTION_STATE_UNAUTHORIZED", + 4: "CONNECTION_STATE_NOT_FOUND", + } + SyncStreamItem_RepoConnectionState_value = map[string]int32{ + "CONNECTION_STATE_UNKNOWN": 0, + "CONNECTION_STATE_PENDING": 1, + "CONNECTION_STATE_CONNECTED": 2, + "CONNECTION_STATE_UNAUTHORIZED": 3, + "CONNECTION_STATE_NOT_FOUND": 4, + } +) + +func (x SyncStreamItem_RepoConnectionState) Enum() *SyncStreamItem_RepoConnectionState { + p := new(SyncStreamItem_RepoConnectionState) + *p = x + return p +} + +func (x SyncStreamItem_RepoConnectionState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SyncStreamItem_RepoConnectionState) Descriptor() protoreflect.EnumDescriptor { + return file_v1_syncservice_proto_enumTypes[1].Descriptor() +} + +func (SyncStreamItem_RepoConnectionState) Type() protoreflect.EnumType { + return &file_v1_syncservice_proto_enumTypes[1] +} + +func (x SyncStreamItem_RepoConnectionState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SyncStreamItem_RepoConnectionState.Descriptor instead. +func (SyncStreamItem_RepoConnectionState) EnumDescriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 0} +} + +type GetRemoteReposResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Repos []*GetRemoteReposResponse_RemoteRepoMetadata `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` +} + +func (x *GetRemoteReposResponse) Reset() { + *x = GetRemoteReposResponse{} + mi := &file_v1_syncservice_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRemoteReposResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRemoteReposResponse) ProtoMessage() {} + +func (x *GetRemoteReposResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRemoteReposResponse.ProtoReflect.Descriptor instead. +func (*GetRemoteReposResponse) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{0} +} + +func (x *GetRemoteReposResponse) GetRepos() []*GetRemoteReposResponse_RemoteRepoMetadata { + if x != nil { + return x.Repos + } + return nil +} + +type SyncStreamItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Action: + // + // *SyncStreamItem_SignedMessage + // *SyncStreamItem_Handshake + // *SyncStreamItem_DiffOperations + // *SyncStreamItem_SendOperations + // *SyncStreamItem_SendConfig + // *SyncStreamItem_EstablishSharedSecret + // *SyncStreamItem_Throttle + Action isSyncStreamItem_Action `protobuf_oneof:"action"` +} + +func (x *SyncStreamItem) Reset() { + *x = SyncStreamItem{} + mi := &file_v1_syncservice_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem) ProtoMessage() {} + +func (x *SyncStreamItem) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem.ProtoReflect.Descriptor instead. +func (*SyncStreamItem) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1} +} + +func (m *SyncStreamItem) GetAction() isSyncStreamItem_Action { + if m != nil { + return m.Action + } + return nil +} + +func (x *SyncStreamItem) GetSignedMessage() *SignedMessage { + if x, ok := x.GetAction().(*SyncStreamItem_SignedMessage); ok { + return x.SignedMessage + } + return nil +} + +func (x *SyncStreamItem) GetHandshake() *SyncStreamItem_SyncActionHandshake { + if x, ok := x.GetAction().(*SyncStreamItem_Handshake); ok { + return x.Handshake + } + return nil +} + +func (x *SyncStreamItem) GetDiffOperations() *SyncStreamItem_SyncActionDiffOperations { + if x, ok := x.GetAction().(*SyncStreamItem_DiffOperations); ok { + return x.DiffOperations + } + return nil +} + +func (x *SyncStreamItem) GetSendOperations() *SyncStreamItem_SyncActionSendOperations { + if x, ok := x.GetAction().(*SyncStreamItem_SendOperations); ok { + return x.SendOperations + } + return nil +} + +func (x *SyncStreamItem) GetSendConfig() *SyncStreamItem_SyncActionSendConfig { + if x, ok := x.GetAction().(*SyncStreamItem_SendConfig); ok { + return x.SendConfig + } + return nil +} + +func (x *SyncStreamItem) GetEstablishSharedSecret() *SyncStreamItem_SyncEstablishSharedSecret { + if x, ok := x.GetAction().(*SyncStreamItem_EstablishSharedSecret); ok { + return x.EstablishSharedSecret + } + return nil +} + +func (x *SyncStreamItem) GetThrottle() *SyncStreamItem_SyncActionThrottle { + if x, ok := x.GetAction().(*SyncStreamItem_Throttle); ok { + return x.Throttle + } + return nil +} + +type isSyncStreamItem_Action interface { + isSyncStreamItem_Action() +} + +type SyncStreamItem_SignedMessage struct { + SignedMessage *SignedMessage `protobuf:"bytes,1,opt,name=signed_message,json=signedMessage,proto3,oneof"` +} + +type SyncStreamItem_Handshake struct { + Handshake *SyncStreamItem_SyncActionHandshake `protobuf:"bytes,3,opt,name=handshake,proto3,oneof"` +} + +type SyncStreamItem_DiffOperations struct { + DiffOperations *SyncStreamItem_SyncActionDiffOperations `protobuf:"bytes,20,opt,name=diff_operations,json=diffOperations,proto3,oneof"` +} + +type SyncStreamItem_SendOperations struct { + SendOperations *SyncStreamItem_SyncActionSendOperations `protobuf:"bytes,21,opt,name=send_operations,json=sendOperations,proto3,oneof"` +} + +type SyncStreamItem_SendConfig struct { + SendConfig *SyncStreamItem_SyncActionSendConfig `protobuf:"bytes,22,opt,name=send_config,json=sendConfig,proto3,oneof"` +} + +type SyncStreamItem_EstablishSharedSecret struct { + EstablishSharedSecret *SyncStreamItem_SyncEstablishSharedSecret `protobuf:"bytes,23,opt,name=establish_shared_secret,json=establishSharedSecret,proto3,oneof"` +} + +type SyncStreamItem_Throttle struct { + Throttle *SyncStreamItem_SyncActionThrottle `protobuf:"bytes,1000,opt,name=throttle,proto3,oneof"` +} + +func (*SyncStreamItem_SignedMessage) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_Handshake) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_DiffOperations) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_SendOperations) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_SendConfig) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_EstablishSharedSecret) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_Throttle) isSyncStreamItem_Action() {} + +// RemoteConfig contains shareable properties from a remote backrest instance. +type RemoteConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Repos []*RemoteRepo `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` +} + +func (x *RemoteConfig) Reset() { + *x = RemoteConfig{} + mi := &file_v1_syncservice_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoteConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoteConfig) ProtoMessage() {} + +func (x *RemoteConfig) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoteConfig.ProtoReflect.Descriptor instead. +func (*RemoteConfig) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{2} +} + +func (x *RemoteConfig) GetRepos() []*RemoteRepo { + if x != nil { + return x.Repos + } + return nil +} + +type RemoteRepo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Guid string `protobuf:"bytes,11,opt,name=guid,proto3" json:"guid,omitempty"` + Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"` + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + Env []string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` + Flags []string `protobuf:"bytes,5,rep,name=flags,proto3" json:"flags,omitempty"` +} + +func (x *RemoteRepo) Reset() { + *x = RemoteRepo{} + mi := &file_v1_syncservice_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoteRepo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoteRepo) ProtoMessage() {} + +func (x *RemoteRepo) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoteRepo.ProtoReflect.Descriptor instead. +func (*RemoteRepo) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{3} +} + +func (x *RemoteRepo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RemoteRepo) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *RemoteRepo) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *RemoteRepo) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *RemoteRepo) GetEnv() []string { + if x != nil { + return x.Env + } + return nil +} + +func (x *RemoteRepo) GetFlags() []string { + if x != nil { + return x.Flags + } + return nil +} + +type GetRemoteReposResponse_RemoteRepoMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstanceId string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` +} + +func (x *GetRemoteReposResponse_RemoteRepoMetadata) Reset() { + *x = GetRemoteReposResponse_RemoteRepoMetadata{} + mi := &file_v1_syncservice_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRemoteReposResponse_RemoteRepoMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRemoteReposResponse_RemoteRepoMetadata) ProtoMessage() {} + +func (x *GetRemoteReposResponse_RemoteRepoMetadata) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRemoteReposResponse_RemoteRepoMetadata.ProtoReflect.Descriptor instead. +func (*GetRemoteReposResponse_RemoteRepoMetadata) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *GetRemoteReposResponse_RemoteRepoMetadata) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *GetRemoteReposResponse_RemoteRepoMetadata) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +type SyncStreamItem_SyncActionHandshake struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProtocolVersion int64 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + PublicKey *PublicKey `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + InstanceId *SignedMessage `protobuf:"bytes,3,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` +} + +func (x *SyncStreamItem_SyncActionHandshake) Reset() { + *x = SyncStreamItem_SyncActionHandshake{} + mi := &file_v1_syncservice_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionHandshake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionHandshake) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionHandshake) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionHandshake.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionHandshake) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *SyncStreamItem_SyncActionHandshake) GetProtocolVersion() int64 { + if x != nil { + return x.ProtocolVersion + } + return 0 +} + +func (x *SyncStreamItem_SyncActionHandshake) GetPublicKey() *PublicKey { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *SyncStreamItem_SyncActionHandshake) GetInstanceId() *SignedMessage { + if x != nil { + return x.InstanceId + } + return nil +} + +type SyncStreamItem_SyncActionSendConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config *RemoteConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` +} + +func (x *SyncStreamItem_SyncActionSendConfig) Reset() { + *x = SyncStreamItem_SyncActionSendConfig{} + mi := &file_v1_syncservice_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionSendConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionSendConfig) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionSendConfig) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionSendConfig.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionSendConfig) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 1} +} + +func (x *SyncStreamItem_SyncActionSendConfig) GetConfig() *RemoteConfig { + if x != nil { + return x.Config + } + return nil +} + +type SyncStreamItem_SyncActionConnectRepo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` +} + +func (x *SyncStreamItem_SyncActionConnectRepo) Reset() { + *x = SyncStreamItem_SyncActionConnectRepo{} + mi := &file_v1_syncservice_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionConnectRepo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionConnectRepo) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionConnectRepo) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionConnectRepo.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionConnectRepo) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 2} +} + +func (x *SyncStreamItem_SyncActionConnectRepo) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +type SyncStreamItem_SyncActionDiffOperations struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Client connects and sends a list of "have_operations" that exist in its log. + // have_operation_ids and have_operation_modnos are the operation IDs and modnos that the client has when zip'd pairwise. + HaveOperationsSelector *OpSelector `protobuf:"bytes,1,opt,name=have_operations_selector,json=haveOperationsSelector,proto3" json:"have_operations_selector,omitempty"` + HaveOperationIds []int64 `protobuf:"varint,2,rep,packed,name=have_operation_ids,json=haveOperationIds,proto3" json:"have_operation_ids,omitempty"` + HaveOperationModnos []int64 `protobuf:"varint,3,rep,packed,name=have_operation_modnos,json=haveOperationModnos,proto3" json:"have_operation_modnos,omitempty"` + // Server sends a list of "request_operations" for any operations that it doesn't have. + RequestOperations []int64 `protobuf:"varint,4,rep,packed,name=request_operations,json=requestOperations,proto3" json:"request_operations,omitempty"` +} + +func (x *SyncStreamItem_SyncActionDiffOperations) Reset() { + *x = SyncStreamItem_SyncActionDiffOperations{} + mi := &file_v1_syncservice_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionDiffOperations) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionDiffOperations) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionDiffOperations) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionDiffOperations.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionDiffOperations) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 3} +} + +func (x *SyncStreamItem_SyncActionDiffOperations) GetHaveOperationsSelector() *OpSelector { + if x != nil { + return x.HaveOperationsSelector + } + return nil +} + +func (x *SyncStreamItem_SyncActionDiffOperations) GetHaveOperationIds() []int64 { + if x != nil { + return x.HaveOperationIds + } + return nil +} + +func (x *SyncStreamItem_SyncActionDiffOperations) GetHaveOperationModnos() []int64 { + if x != nil { + return x.HaveOperationModnos + } + return nil +} + +func (x *SyncStreamItem_SyncActionDiffOperations) GetRequestOperations() []int64 { + if x != nil { + return x.RequestOperations + } + return nil +} + +type SyncStreamItem_SyncActionSendOperations struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Event *OperationEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"` +} + +func (x *SyncStreamItem_SyncActionSendOperations) Reset() { + *x = SyncStreamItem_SyncActionSendOperations{} + mi := &file_v1_syncservice_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionSendOperations) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionSendOperations) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionSendOperations) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionSendOperations.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionSendOperations) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 4} +} + +func (x *SyncStreamItem_SyncActionSendOperations) GetEvent() *OperationEvent { + if x != nil { + return x.Event + } + return nil +} + +type SyncStreamItem_SyncActionThrottle struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DelayMs int64 `protobuf:"varint,1,opt,name=delay_ms,json=delayMs,proto3" json:"delay_ms,omitempty"` +} + +func (x *SyncStreamItem_SyncActionThrottle) Reset() { + *x = SyncStreamItem_SyncActionThrottle{} + mi := &file_v1_syncservice_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionThrottle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionThrottle) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionThrottle) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionThrottle.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionThrottle) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 5} +} + +func (x *SyncStreamItem_SyncActionThrottle) GetDelayMs() int64 { + if x != nil { + return x.DelayMs + } + return 0 +} + +type SyncStreamItem_SyncEstablishSharedSecret struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // a one-time-use ed25519 public key with a matching unshared private key. Used to perform a key exchange. + // See https://pkg.go.dev/crypto/ecdh#PrivateKey.ECDH . + Ed25519 string `protobuf:"bytes,2,opt,name=ed25519,json=ed25519pub,proto3" json:"ed25519,omitempty"` // base64 encoded public key +} + +func (x *SyncStreamItem_SyncEstablishSharedSecret) Reset() { + *x = SyncStreamItem_SyncEstablishSharedSecret{} + mi := &file_v1_syncservice_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncEstablishSharedSecret) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncEstablishSharedSecret) ProtoMessage() {} + +func (x *SyncStreamItem_SyncEstablishSharedSecret) ProtoReflect() protoreflect.Message { + mi := &file_v1_syncservice_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncEstablishSharedSecret.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncEstablishSharedSecret) Descriptor() ([]byte, []int) { + return file_v1_syncservice_proto_rawDescGZIP(), []int{1, 6} +} + +func (x *SyncStreamItem_SyncEstablishSharedSecret) GetEd25519() string { + if x != nil { + return x.Ed25519 + } + return "" +} + +var File_v1_syncservice_proto protoreflect.FileDescriptor + +var file_v1_syncservice_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x76, 0x31, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, + 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x10, 0x76, + 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xad, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, + 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x05, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x1a, 0x4e, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, + 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, + 0x49, 0x64, 0x22, 0xc1, 0x0b, 0x0a, 0x0e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x3a, 0x0a, 0x0e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x48, 0x00, 0x52, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x46, 0x0a, 0x09, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x48, 0x00, 0x52, 0x09, + 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x56, 0x0a, 0x0f, 0x64, 0x69, 0x66, + 0x66, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x44, 0x69, 0x66, 0x66, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, + 0x00, 0x52, 0x0e, 0x64, 0x69, 0x66, 0x66, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x56, 0x0a, 0x0f, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x2e, 0x53, + 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, 0x0b, 0x73, 0x65, 0x6e, + 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x74, + 0x65, 0x6d, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6e, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x66, 0x0a, 0x17, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, + 0x73, 0x68, 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x45, + 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x48, 0x00, 0x52, 0x15, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x44, 0x0a, + 0x08, 0x74, 0x68, 0x72, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x18, 0xe8, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x49, 0x74, 0x65, 0x6d, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x68, 0x72, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x08, 0x74, 0x68, 0x72, 0x6f, 0x74, + 0x74, 0x6c, 0x65, 0x1a, 0xa2, 0x01, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0a, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x40, 0x0a, 0x14, 0x53, 0x79, 0x6e, 0x63, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x28, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x30, 0x0a, 0x15, 0x53, 0x79, + 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x1a, 0xf5, 0x01, 0x0a, + 0x18, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x66, 0x66, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x18, 0x68, 0x61, 0x76, + 0x65, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x70, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x16, 0x68, 0x61, 0x76, + 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x61, 0x76, 0x65, 0x5f, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, + 0x10, 0x68, 0x61, 0x76, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x73, 0x12, 0x32, 0x0a, 0x15, 0x68, 0x61, 0x76, 0x65, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x6f, 0x64, 0x6e, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, + 0x52, 0x13, 0x68, 0x61, 0x76, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, + 0x6f, 0x64, 0x6e, 0x6f, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x03, 0x52, 0x11, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x44, 0x0a, 0x18, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x28, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x2f, 0x0a, 0x12, 0x53, 0x79, + 0x6e, 0x63, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x68, 0x72, 0x6f, 0x74, 0x74, 0x6c, 0x65, + 0x12, 0x19, 0x0a, 0x08, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x4d, 0x73, 0x1a, 0x38, 0x0a, 0x19, 0x53, + 0x79, 0x6e, 0x63, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x68, 0x61, 0x72, + 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x07, 0x65, 0x64, 0x32, 0x35, + 0x35, 0x31, 0x39, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x64, 0x32, 0x35, 0x35, + 0x31, 0x39, 0x70, 0x75, 0x62, 0x22, 0xb4, 0x01, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, + 0x18, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x43, + 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, + 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, + 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, + 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x04, 0x42, 0x08, 0x0a, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x34, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x22, 0x86, 0x01, 0x0a, + 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x67, + 0x75, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, + 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, + 0x69, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x65, 0x6e, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, + 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, + 0x66, 0x6c, 0x61, 0x67, 0x73, 0x2a, 0xfb, 0x01, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, + 0x18, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x43, + 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, + 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x49, + 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, + 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x57, 0x41, 0x49, 0x54, 0x10, 0x04, 0x12, 0x1f, 0x0a, + 0x1b, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x10, 0x0a, 0x12, 0x23, + 0x0a, 0x1f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, + 0x4c, 0x10, 0x0b, 0x32, 0x93, 0x01, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, + 0x53, 0x79, 0x6e, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x04, 0x53, + 0x79, 0x6e, 0x63, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, + 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x74, 0x65, 0x6d, 0x22, 0x00, 0x28, 0x01, 0x30, + 0x01, 0x12, 0x46, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, + 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_syncservice_proto_rawDescOnce sync.Once + file_v1_syncservice_proto_rawDescData = file_v1_syncservice_proto_rawDesc +) + +func file_v1_syncservice_proto_rawDescGZIP() []byte { + file_v1_syncservice_proto_rawDescOnce.Do(func() { + file_v1_syncservice_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_syncservice_proto_rawDescData) + }) + return file_v1_syncservice_proto_rawDescData +} + +var file_v1_syncservice_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_v1_syncservice_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_v1_syncservice_proto_goTypes = []any{ + (SyncConnectionState)(0), // 0: v1.SyncConnectionState + (SyncStreamItem_RepoConnectionState)(0), // 1: v1.SyncStreamItem.RepoConnectionState + (*GetRemoteReposResponse)(nil), // 2: v1.GetRemoteReposResponse + (*SyncStreamItem)(nil), // 3: v1.SyncStreamItem + (*RemoteConfig)(nil), // 4: v1.RemoteConfig + (*RemoteRepo)(nil), // 5: v1.RemoteRepo + (*GetRemoteReposResponse_RemoteRepoMetadata)(nil), // 6: v1.GetRemoteReposResponse.RemoteRepoMetadata + (*SyncStreamItem_SyncActionHandshake)(nil), // 7: v1.SyncStreamItem.SyncActionHandshake + (*SyncStreamItem_SyncActionSendConfig)(nil), // 8: v1.SyncStreamItem.SyncActionSendConfig + (*SyncStreamItem_SyncActionConnectRepo)(nil), // 9: v1.SyncStreamItem.SyncActionConnectRepo + (*SyncStreamItem_SyncActionDiffOperations)(nil), // 10: v1.SyncStreamItem.SyncActionDiffOperations + (*SyncStreamItem_SyncActionSendOperations)(nil), // 11: v1.SyncStreamItem.SyncActionSendOperations + (*SyncStreamItem_SyncActionThrottle)(nil), // 12: v1.SyncStreamItem.SyncActionThrottle + (*SyncStreamItem_SyncEstablishSharedSecret)(nil), // 13: v1.SyncStreamItem.SyncEstablishSharedSecret + (*SignedMessage)(nil), // 14: v1.SignedMessage + (*PublicKey)(nil), // 15: v1.PublicKey + (*OpSelector)(nil), // 16: v1.OpSelector + (*OperationEvent)(nil), // 17: v1.OperationEvent + (*emptypb.Empty)(nil), // 18: google.protobuf.Empty +} +var file_v1_syncservice_proto_depIdxs = []int32{ + 6, // 0: v1.GetRemoteReposResponse.repos:type_name -> v1.GetRemoteReposResponse.RemoteRepoMetadata + 14, // 1: v1.SyncStreamItem.signed_message:type_name -> v1.SignedMessage + 7, // 2: v1.SyncStreamItem.handshake:type_name -> v1.SyncStreamItem.SyncActionHandshake + 10, // 3: v1.SyncStreamItem.diff_operations:type_name -> v1.SyncStreamItem.SyncActionDiffOperations + 11, // 4: v1.SyncStreamItem.send_operations:type_name -> v1.SyncStreamItem.SyncActionSendOperations + 8, // 5: v1.SyncStreamItem.send_config:type_name -> v1.SyncStreamItem.SyncActionSendConfig + 13, // 6: v1.SyncStreamItem.establish_shared_secret:type_name -> v1.SyncStreamItem.SyncEstablishSharedSecret + 12, // 7: v1.SyncStreamItem.throttle:type_name -> v1.SyncStreamItem.SyncActionThrottle + 5, // 8: v1.RemoteConfig.repos:type_name -> v1.RemoteRepo + 15, // 9: v1.SyncStreamItem.SyncActionHandshake.public_key:type_name -> v1.PublicKey + 14, // 10: v1.SyncStreamItem.SyncActionHandshake.instance_id:type_name -> v1.SignedMessage + 4, // 11: v1.SyncStreamItem.SyncActionSendConfig.config:type_name -> v1.RemoteConfig + 16, // 12: v1.SyncStreamItem.SyncActionDiffOperations.have_operations_selector:type_name -> v1.OpSelector + 17, // 13: v1.SyncStreamItem.SyncActionSendOperations.event:type_name -> v1.OperationEvent + 3, // 14: v1.BackrestSyncService.Sync:input_type -> v1.SyncStreamItem + 18, // 15: v1.BackrestSyncService.GetRemoteRepos:input_type -> google.protobuf.Empty + 3, // 16: v1.BackrestSyncService.Sync:output_type -> v1.SyncStreamItem + 2, // 17: v1.BackrestSyncService.GetRemoteRepos:output_type -> v1.GetRemoteReposResponse + 16, // [16:18] is the sub-list for method output_type + 14, // [14:16] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_v1_syncservice_proto_init() } +func file_v1_syncservice_proto_init() { + if File_v1_syncservice_proto != nil { + return + } + file_v1_config_proto_init() + file_v1_crypto_proto_init() + file_v1_restic_proto_init() + file_v1_service_proto_init() + file_v1_operations_proto_init() + file_v1_syncservice_proto_msgTypes[1].OneofWrappers = []any{ + (*SyncStreamItem_SignedMessage)(nil), + (*SyncStreamItem_Handshake)(nil), + (*SyncStreamItem_DiffOperations)(nil), + (*SyncStreamItem_SendOperations)(nil), + (*SyncStreamItem_SendConfig)(nil), + (*SyncStreamItem_EstablishSharedSecret)(nil), + (*SyncStreamItem_Throttle)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_syncservice_proto_rawDesc, + NumEnums: 2, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_v1_syncservice_proto_goTypes, + DependencyIndexes: file_v1_syncservice_proto_depIdxs, + EnumInfos: file_v1_syncservice_proto_enumTypes, + MessageInfos: file_v1_syncservice_proto_msgTypes, + }.Build() + File_v1_syncservice_proto = out.File + file_v1_syncservice_proto_rawDesc = nil + file_v1_syncservice_proto_goTypes = nil + file_v1_syncservice_proto_depIdxs = nil +} diff --git a/gen/go/v1/syncservice_grpc.pb.go b/gen/go/v1/syncservice_grpc.pb.go new file mode 100644 index 000000000..1ceed7b4c --- /dev/null +++ b/gen/go/v1/syncservice_grpc.pb.go @@ -0,0 +1,155 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: v1/syncservice.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + BackrestSyncService_Sync_FullMethodName = "/v1.BackrestSyncService/Sync" + BackrestSyncService_GetRemoteRepos_FullMethodName = "/v1.BackrestSyncService/GetRemoteRepos" +) + +// BackrestSyncServiceClient is the client API for BackrestSyncService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type BackrestSyncServiceClient interface { + Sync(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SyncStreamItem, SyncStreamItem], error) + GetRemoteRepos(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetRemoteReposResponse, error) +} + +type backrestSyncServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewBackrestSyncServiceClient(cc grpc.ClientConnInterface) BackrestSyncServiceClient { + return &backrestSyncServiceClient{cc} +} + +func (c *backrestSyncServiceClient) Sync(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SyncStreamItem, SyncStreamItem], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &BackrestSyncService_ServiceDesc.Streams[0], BackrestSyncService_Sync_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SyncStreamItem, SyncStreamItem]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type BackrestSyncService_SyncClient = grpc.BidiStreamingClient[SyncStreamItem, SyncStreamItem] + +func (c *backrestSyncServiceClient) GetRemoteRepos(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetRemoteReposResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRemoteReposResponse) + err := c.cc.Invoke(ctx, BackrestSyncService_GetRemoteRepos_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// BackrestSyncServiceServer is the server API for BackrestSyncService service. +// All implementations must embed UnimplementedBackrestSyncServiceServer +// for forward compatibility. +type BackrestSyncServiceServer interface { + Sync(grpc.BidiStreamingServer[SyncStreamItem, SyncStreamItem]) error + GetRemoteRepos(context.Context, *emptypb.Empty) (*GetRemoteReposResponse, error) + mustEmbedUnimplementedBackrestSyncServiceServer() +} + +// UnimplementedBackrestSyncServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedBackrestSyncServiceServer struct{} + +func (UnimplementedBackrestSyncServiceServer) Sync(grpc.BidiStreamingServer[SyncStreamItem, SyncStreamItem]) error { + return status.Errorf(codes.Unimplemented, "method Sync not implemented") +} +func (UnimplementedBackrestSyncServiceServer) GetRemoteRepos(context.Context, *emptypb.Empty) (*GetRemoteReposResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRemoteRepos not implemented") +} +func (UnimplementedBackrestSyncServiceServer) mustEmbedUnimplementedBackrestSyncServiceServer() {} +func (UnimplementedBackrestSyncServiceServer) testEmbeddedByValue() {} + +// UnsafeBackrestSyncServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to BackrestSyncServiceServer will +// result in compilation errors. +type UnsafeBackrestSyncServiceServer interface { + mustEmbedUnimplementedBackrestSyncServiceServer() +} + +func RegisterBackrestSyncServiceServer(s grpc.ServiceRegistrar, srv BackrestSyncServiceServer) { + // If the following call pancis, it indicates UnimplementedBackrestSyncServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&BackrestSyncService_ServiceDesc, srv) +} + +func _BackrestSyncService_Sync_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(BackrestSyncServiceServer).Sync(&grpc.GenericServerStream[SyncStreamItem, SyncStreamItem]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type BackrestSyncService_SyncServer = grpc.BidiStreamingServer[SyncStreamItem, SyncStreamItem] + +func _BackrestSyncService_GetRemoteRepos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestSyncServiceServer).GetRemoteRepos(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: BackrestSyncService_GetRemoteRepos_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestSyncServiceServer).GetRemoteRepos(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// BackrestSyncService_ServiceDesc is the grpc.ServiceDesc for BackrestSyncService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var BackrestSyncService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "v1.BackrestSyncService", + HandlerType: (*BackrestSyncServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetRemoteRepos", + Handler: _BackrestSyncService_GetRemoteRepos_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Sync", + Handler: _BackrestSyncService_Sync_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "v1/syncservice.proto", +} diff --git a/gen/go/v1/v1connect/authentication.connect.go b/gen/go/v1/v1connect/authentication.connect.go new file mode 100644 index 000000000..b8ce7ab7b --- /dev/null +++ b/gen/go/v1/v1connect/authentication.connect.go @@ -0,0 +1,143 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: v1/authentication.proto + +package v1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + types "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // AuthenticationName is the fully-qualified name of the Authentication service. + AuthenticationName = "v1.Authentication" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // AuthenticationLoginProcedure is the fully-qualified name of the Authentication's Login RPC. + AuthenticationLoginProcedure = "/v1.Authentication/Login" + // AuthenticationHashPasswordProcedure is the fully-qualified name of the Authentication's + // HashPassword RPC. + AuthenticationHashPasswordProcedure = "/v1.Authentication/HashPassword" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + authenticationServiceDescriptor = v1.File_v1_authentication_proto.Services().ByName("Authentication") + authenticationLoginMethodDescriptor = authenticationServiceDescriptor.Methods().ByName("Login") + authenticationHashPasswordMethodDescriptor = authenticationServiceDescriptor.Methods().ByName("HashPassword") +) + +// AuthenticationClient is a client for the v1.Authentication service. +type AuthenticationClient interface { + Login(context.Context, *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) + HashPassword(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringValue], error) +} + +// NewAuthenticationClient constructs a client for the v1.Authentication service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewAuthenticationClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthenticationClient { + baseURL = strings.TrimRight(baseURL, "/") + return &authenticationClient{ + login: connect.NewClient[v1.LoginRequest, v1.LoginResponse]( + httpClient, + baseURL+AuthenticationLoginProcedure, + connect.WithSchema(authenticationLoginMethodDescriptor), + connect.WithClientOptions(opts...), + ), + hashPassword: connect.NewClient[types.StringValue, types.StringValue]( + httpClient, + baseURL+AuthenticationHashPasswordProcedure, + connect.WithSchema(authenticationHashPasswordMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// authenticationClient implements AuthenticationClient. +type authenticationClient struct { + login *connect.Client[v1.LoginRequest, v1.LoginResponse] + hashPassword *connect.Client[types.StringValue, types.StringValue] +} + +// Login calls v1.Authentication.Login. +func (c *authenticationClient) Login(ctx context.Context, req *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) { + return c.login.CallUnary(ctx, req) +} + +// HashPassword calls v1.Authentication.HashPassword. +func (c *authenticationClient) HashPassword(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[types.StringValue], error) { + return c.hashPassword.CallUnary(ctx, req) +} + +// AuthenticationHandler is an implementation of the v1.Authentication service. +type AuthenticationHandler interface { + Login(context.Context, *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) + HashPassword(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringValue], error) +} + +// NewAuthenticationHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewAuthenticationHandler(svc AuthenticationHandler, opts ...connect.HandlerOption) (string, http.Handler) { + authenticationLoginHandler := connect.NewUnaryHandler( + AuthenticationLoginProcedure, + svc.Login, + connect.WithSchema(authenticationLoginMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + authenticationHashPasswordHandler := connect.NewUnaryHandler( + AuthenticationHashPasswordProcedure, + svc.HashPassword, + connect.WithSchema(authenticationHashPasswordMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/v1.Authentication/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case AuthenticationLoginProcedure: + authenticationLoginHandler.ServeHTTP(w, r) + case AuthenticationHashPasswordProcedure: + authenticationHashPasswordHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedAuthenticationHandler returns CodeUnimplemented from all methods. +type UnimplementedAuthenticationHandler struct{} + +func (UnimplementedAuthenticationHandler) Login(context.Context, *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Authentication.Login is not implemented")) +} + +func (UnimplementedAuthenticationHandler) HashPassword(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringValue], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Authentication.HashPassword is not implemented")) +} diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go new file mode 100644 index 000000000..41af420bb --- /dev/null +++ b/gen/go/v1/v1connect/service.connect.go @@ -0,0 +1,692 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: v1/service.proto + +package v1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + types "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // BackrestName is the fully-qualified name of the Backrest service. + BackrestName = "v1.Backrest" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // BackrestGetConfigProcedure is the fully-qualified name of the Backrest's GetConfig RPC. + BackrestGetConfigProcedure = "/v1.Backrest/GetConfig" + // BackrestSetConfigProcedure is the fully-qualified name of the Backrest's SetConfig RPC. + BackrestSetConfigProcedure = "/v1.Backrest/SetConfig" + // BackrestCheckRepoExistsProcedure is the fully-qualified name of the Backrest's CheckRepoExists + // RPC. + BackrestCheckRepoExistsProcedure = "/v1.Backrest/CheckRepoExists" + // BackrestAddRepoProcedure is the fully-qualified name of the Backrest's AddRepo RPC. + BackrestAddRepoProcedure = "/v1.Backrest/AddRepo" + // BackrestRemoveRepoProcedure is the fully-qualified name of the Backrest's RemoveRepo RPC. + BackrestRemoveRepoProcedure = "/v1.Backrest/RemoveRepo" + // BackrestGetOperationEventsProcedure is the fully-qualified name of the Backrest's + // GetOperationEvents RPC. + BackrestGetOperationEventsProcedure = "/v1.Backrest/GetOperationEvents" + // BackrestGetOperationsProcedure is the fully-qualified name of the Backrest's GetOperations RPC. + BackrestGetOperationsProcedure = "/v1.Backrest/GetOperations" + // BackrestListSnapshotsProcedure is the fully-qualified name of the Backrest's ListSnapshots RPC. + BackrestListSnapshotsProcedure = "/v1.Backrest/ListSnapshots" + // BackrestListSnapshotFilesProcedure is the fully-qualified name of the Backrest's + // ListSnapshotFiles RPC. + BackrestListSnapshotFilesProcedure = "/v1.Backrest/ListSnapshotFiles" + // BackrestBackupProcedure is the fully-qualified name of the Backrest's Backup RPC. + BackrestBackupProcedure = "/v1.Backrest/Backup" + // BackrestDoRepoTaskProcedure is the fully-qualified name of the Backrest's DoRepoTask RPC. + BackrestDoRepoTaskProcedure = "/v1.Backrest/DoRepoTask" + // BackrestForgetProcedure is the fully-qualified name of the Backrest's Forget RPC. + BackrestForgetProcedure = "/v1.Backrest/Forget" + // BackrestRestoreProcedure is the fully-qualified name of the Backrest's Restore RPC. + BackrestRestoreProcedure = "/v1.Backrest/Restore" + // BackrestCancelProcedure is the fully-qualified name of the Backrest's Cancel RPC. + BackrestCancelProcedure = "/v1.Backrest/Cancel" + // BackrestGetLogsProcedure is the fully-qualified name of the Backrest's GetLogs RPC. + BackrestGetLogsProcedure = "/v1.Backrest/GetLogs" + // BackrestRunCommandProcedure is the fully-qualified name of the Backrest's RunCommand RPC. + BackrestRunCommandProcedure = "/v1.Backrest/RunCommand" + // BackrestGetDownloadURLProcedure is the fully-qualified name of the Backrest's GetDownloadURL RPC. + BackrestGetDownloadURLProcedure = "/v1.Backrest/GetDownloadURL" + // BackrestClearHistoryProcedure is the fully-qualified name of the Backrest's ClearHistory RPC. + BackrestClearHistoryProcedure = "/v1.Backrest/ClearHistory" + // BackrestPathAutocompleteProcedure is the fully-qualified name of the Backrest's PathAutocomplete + // RPC. + BackrestPathAutocompleteProcedure = "/v1.Backrest/PathAutocomplete" + // BackrestGetSummaryDashboardProcedure is the fully-qualified name of the Backrest's + // GetSummaryDashboard RPC. + BackrestGetSummaryDashboardProcedure = "/v1.Backrest/GetSummaryDashboard" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + backrestServiceDescriptor = v1.File_v1_service_proto.Services().ByName("Backrest") + backrestGetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetConfig") + backrestSetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("SetConfig") + backrestCheckRepoExistsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("CheckRepoExists") + backrestAddRepoMethodDescriptor = backrestServiceDescriptor.Methods().ByName("AddRepo") + backrestRemoveRepoMethodDescriptor = backrestServiceDescriptor.Methods().ByName("RemoveRepo") + backrestGetOperationEventsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperationEvents") + backrestGetOperationsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperations") + backrestListSnapshotsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshots") + backrestListSnapshotFilesMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshotFiles") + backrestBackupMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Backup") + backrestDoRepoTaskMethodDescriptor = backrestServiceDescriptor.Methods().ByName("DoRepoTask") + backrestForgetMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Forget") + backrestRestoreMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Restore") + backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel") + backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs") + backrestRunCommandMethodDescriptor = backrestServiceDescriptor.Methods().ByName("RunCommand") + backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL") + backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory") + backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete") + backrestGetSummaryDashboardMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetSummaryDashboard") +) + +// BackrestClient is a client for the v1.Backrest service. +type BackrestClient interface { + GetConfig(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) + SetConfig(context.Context, *connect.Request[v1.Config]) (*connect.Response[v1.Config], error) + CheckRepoExists(context.Context, *connect.Request[v1.Repo]) (*connect.Response[types.BoolValue], error) + AddRepo(context.Context, *connect.Request[v1.Repo]) (*connect.Response[v1.Config], error) + RemoveRepo(context.Context, *connect.Request[types.StringValue]) (*connect.Response[v1.Config], error) + GetOperationEvents(context.Context, *connect.Request[emptypb.Empty]) (*connect.ServerStreamForClient[v1.OperationEvent], error) + GetOperations(context.Context, *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) + ListSnapshots(context.Context, *connect.Request[v1.ListSnapshotsRequest]) (*connect.Response[v1.ResticSnapshotList], error) + ListSnapshotFiles(context.Context, *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) + // DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + DoRepoTask(context.Context, *connect.Request[v1.DoRepoTaskRequest]) (*connect.Response[emptypb.Empty], error) + // Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + Forget(context.Context, *connect.Request[v1.ForgetRequest]) (*connect.Response[emptypb.Empty], error) + // Restore schedules a restore operation. + Restore(context.Context, *connect.Request[v1.RestoreSnapshotRequest]) (*connect.Response[emptypb.Empty], error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) + // GetLogs returns the keyed large data for the given operation. + GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) + // RunCommand executes a generic restic command on the repository. + RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) + // GetDownloadURL returns a signed download URL given a forget operation ID. + GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) + // Clears the history of operations + ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. + PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) +} + +// NewBackrestClient constructs a client for the v1.Backrest service. By default, it uses the +// Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) BackrestClient { + baseURL = strings.TrimRight(baseURL, "/") + return &backrestClient{ + getConfig: connect.NewClient[emptypb.Empty, v1.Config]( + httpClient, + baseURL+BackrestGetConfigProcedure, + connect.WithSchema(backrestGetConfigMethodDescriptor), + connect.WithClientOptions(opts...), + ), + setConfig: connect.NewClient[v1.Config, v1.Config]( + httpClient, + baseURL+BackrestSetConfigProcedure, + connect.WithSchema(backrestSetConfigMethodDescriptor), + connect.WithClientOptions(opts...), + ), + checkRepoExists: connect.NewClient[v1.Repo, types.BoolValue]( + httpClient, + baseURL+BackrestCheckRepoExistsProcedure, + connect.WithSchema(backrestCheckRepoExistsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + addRepo: connect.NewClient[v1.Repo, v1.Config]( + httpClient, + baseURL+BackrestAddRepoProcedure, + connect.WithSchema(backrestAddRepoMethodDescriptor), + connect.WithClientOptions(opts...), + ), + removeRepo: connect.NewClient[types.StringValue, v1.Config]( + httpClient, + baseURL+BackrestRemoveRepoProcedure, + connect.WithSchema(backrestRemoveRepoMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getOperationEvents: connect.NewClient[emptypb.Empty, v1.OperationEvent]( + httpClient, + baseURL+BackrestGetOperationEventsProcedure, + connect.WithSchema(backrestGetOperationEventsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getOperations: connect.NewClient[v1.GetOperationsRequest, v1.OperationList]( + httpClient, + baseURL+BackrestGetOperationsProcedure, + connect.WithSchema(backrestGetOperationsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + listSnapshots: connect.NewClient[v1.ListSnapshotsRequest, v1.ResticSnapshotList]( + httpClient, + baseURL+BackrestListSnapshotsProcedure, + connect.WithSchema(backrestListSnapshotsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + listSnapshotFiles: connect.NewClient[v1.ListSnapshotFilesRequest, v1.ListSnapshotFilesResponse]( + httpClient, + baseURL+BackrestListSnapshotFilesProcedure, + connect.WithSchema(backrestListSnapshotFilesMethodDescriptor), + connect.WithClientOptions(opts...), + ), + backup: connect.NewClient[types.StringValue, emptypb.Empty]( + httpClient, + baseURL+BackrestBackupProcedure, + connect.WithSchema(backrestBackupMethodDescriptor), + connect.WithClientOptions(opts...), + ), + doRepoTask: connect.NewClient[v1.DoRepoTaskRequest, emptypb.Empty]( + httpClient, + baseURL+BackrestDoRepoTaskProcedure, + connect.WithSchema(backrestDoRepoTaskMethodDescriptor), + connect.WithClientOptions(opts...), + ), + forget: connect.NewClient[v1.ForgetRequest, emptypb.Empty]( + httpClient, + baseURL+BackrestForgetProcedure, + connect.WithSchema(backrestForgetMethodDescriptor), + connect.WithClientOptions(opts...), + ), + restore: connect.NewClient[v1.RestoreSnapshotRequest, emptypb.Empty]( + httpClient, + baseURL+BackrestRestoreProcedure, + connect.WithSchema(backrestRestoreMethodDescriptor), + connect.WithClientOptions(opts...), + ), + cancel: connect.NewClient[types.Int64Value, emptypb.Empty]( + httpClient, + baseURL+BackrestCancelProcedure, + connect.WithSchema(backrestCancelMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getLogs: connect.NewClient[v1.LogDataRequest, types.BytesValue]( + httpClient, + baseURL+BackrestGetLogsProcedure, + connect.WithSchema(backrestGetLogsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + runCommand: connect.NewClient[v1.RunCommandRequest, types.Int64Value]( + httpClient, + baseURL+BackrestRunCommandProcedure, + connect.WithSchema(backrestRunCommandMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getDownloadURL: connect.NewClient[types.Int64Value, types.StringValue]( + httpClient, + baseURL+BackrestGetDownloadURLProcedure, + connect.WithSchema(backrestGetDownloadURLMethodDescriptor), + connect.WithClientOptions(opts...), + ), + clearHistory: connect.NewClient[v1.ClearHistoryRequest, emptypb.Empty]( + httpClient, + baseURL+BackrestClearHistoryProcedure, + connect.WithSchema(backrestClearHistoryMethodDescriptor), + connect.WithClientOptions(opts...), + ), + pathAutocomplete: connect.NewClient[types.StringValue, types.StringList]( + httpClient, + baseURL+BackrestPathAutocompleteProcedure, + connect.WithSchema(backrestPathAutocompleteMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getSummaryDashboard: connect.NewClient[emptypb.Empty, v1.SummaryDashboardResponse]( + httpClient, + baseURL+BackrestGetSummaryDashboardProcedure, + connect.WithSchema(backrestGetSummaryDashboardMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// backrestClient implements BackrestClient. +type backrestClient struct { + getConfig *connect.Client[emptypb.Empty, v1.Config] + setConfig *connect.Client[v1.Config, v1.Config] + checkRepoExists *connect.Client[v1.Repo, types.BoolValue] + addRepo *connect.Client[v1.Repo, v1.Config] + removeRepo *connect.Client[types.StringValue, v1.Config] + getOperationEvents *connect.Client[emptypb.Empty, v1.OperationEvent] + getOperations *connect.Client[v1.GetOperationsRequest, v1.OperationList] + listSnapshots *connect.Client[v1.ListSnapshotsRequest, v1.ResticSnapshotList] + listSnapshotFiles *connect.Client[v1.ListSnapshotFilesRequest, v1.ListSnapshotFilesResponse] + backup *connect.Client[types.StringValue, emptypb.Empty] + doRepoTask *connect.Client[v1.DoRepoTaskRequest, emptypb.Empty] + forget *connect.Client[v1.ForgetRequest, emptypb.Empty] + restore *connect.Client[v1.RestoreSnapshotRequest, emptypb.Empty] + cancel *connect.Client[types.Int64Value, emptypb.Empty] + getLogs *connect.Client[v1.LogDataRequest, types.BytesValue] + runCommand *connect.Client[v1.RunCommandRequest, types.Int64Value] + getDownloadURL *connect.Client[types.Int64Value, types.StringValue] + clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty] + pathAutocomplete *connect.Client[types.StringValue, types.StringList] + getSummaryDashboard *connect.Client[emptypb.Empty, v1.SummaryDashboardResponse] +} + +// GetConfig calls v1.Backrest.GetConfig. +func (c *backrestClient) GetConfig(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) { + return c.getConfig.CallUnary(ctx, req) +} + +// SetConfig calls v1.Backrest.SetConfig. +func (c *backrestClient) SetConfig(ctx context.Context, req *connect.Request[v1.Config]) (*connect.Response[v1.Config], error) { + return c.setConfig.CallUnary(ctx, req) +} + +// CheckRepoExists calls v1.Backrest.CheckRepoExists. +func (c *backrestClient) CheckRepoExists(ctx context.Context, req *connect.Request[v1.Repo]) (*connect.Response[types.BoolValue], error) { + return c.checkRepoExists.CallUnary(ctx, req) +} + +// AddRepo calls v1.Backrest.AddRepo. +func (c *backrestClient) AddRepo(ctx context.Context, req *connect.Request[v1.Repo]) (*connect.Response[v1.Config], error) { + return c.addRepo.CallUnary(ctx, req) +} + +// RemoveRepo calls v1.Backrest.RemoveRepo. +func (c *backrestClient) RemoveRepo(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[v1.Config], error) { + return c.removeRepo.CallUnary(ctx, req) +} + +// GetOperationEvents calls v1.Backrest.GetOperationEvents. +func (c *backrestClient) GetOperationEvents(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.ServerStreamForClient[v1.OperationEvent], error) { + return c.getOperationEvents.CallServerStream(ctx, req) +} + +// GetOperations calls v1.Backrest.GetOperations. +func (c *backrestClient) GetOperations(ctx context.Context, req *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) { + return c.getOperations.CallUnary(ctx, req) +} + +// ListSnapshots calls v1.Backrest.ListSnapshots. +func (c *backrestClient) ListSnapshots(ctx context.Context, req *connect.Request[v1.ListSnapshotsRequest]) (*connect.Response[v1.ResticSnapshotList], error) { + return c.listSnapshots.CallUnary(ctx, req) +} + +// ListSnapshotFiles calls v1.Backrest.ListSnapshotFiles. +func (c *backrestClient) ListSnapshotFiles(ctx context.Context, req *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) { + return c.listSnapshotFiles.CallUnary(ctx, req) +} + +// Backup calls v1.Backrest.Backup. +func (c *backrestClient) Backup(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) { + return c.backup.CallUnary(ctx, req) +} + +// DoRepoTask calls v1.Backrest.DoRepoTask. +func (c *backrestClient) DoRepoTask(ctx context.Context, req *connect.Request[v1.DoRepoTaskRequest]) (*connect.Response[emptypb.Empty], error) { + return c.doRepoTask.CallUnary(ctx, req) +} + +// Forget calls v1.Backrest.Forget. +func (c *backrestClient) Forget(ctx context.Context, req *connect.Request[v1.ForgetRequest]) (*connect.Response[emptypb.Empty], error) { + return c.forget.CallUnary(ctx, req) +} + +// Restore calls v1.Backrest.Restore. +func (c *backrestClient) Restore(ctx context.Context, req *connect.Request[v1.RestoreSnapshotRequest]) (*connect.Response[emptypb.Empty], error) { + return c.restore.CallUnary(ctx, req) +} + +// Cancel calls v1.Backrest.Cancel. +func (c *backrestClient) Cancel(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) { + return c.cancel.CallUnary(ctx, req) +} + +// GetLogs calls v1.Backrest.GetLogs. +func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) { + return c.getLogs.CallServerStream(ctx, req) +} + +// RunCommand calls v1.Backrest.RunCommand. +func (c *backrestClient) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + return c.runCommand.CallUnary(ctx, req) +} + +// GetDownloadURL calls v1.Backrest.GetDownloadURL. +func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { + return c.getDownloadURL.CallUnary(ctx, req) +} + +// ClearHistory calls v1.Backrest.ClearHistory. +func (c *backrestClient) ClearHistory(ctx context.Context, req *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) { + return c.clearHistory.CallUnary(ctx, req) +} + +// PathAutocomplete calls v1.Backrest.PathAutocomplete. +func (c *backrestClient) PathAutocomplete(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) { + return c.pathAutocomplete.CallUnary(ctx, req) +} + +// GetSummaryDashboard calls v1.Backrest.GetSummaryDashboard. +func (c *backrestClient) GetSummaryDashboard(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + return c.getSummaryDashboard.CallUnary(ctx, req) +} + +// BackrestHandler is an implementation of the v1.Backrest service. +type BackrestHandler interface { + GetConfig(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) + SetConfig(context.Context, *connect.Request[v1.Config]) (*connect.Response[v1.Config], error) + CheckRepoExists(context.Context, *connect.Request[v1.Repo]) (*connect.Response[types.BoolValue], error) + AddRepo(context.Context, *connect.Request[v1.Repo]) (*connect.Response[v1.Config], error) + RemoveRepo(context.Context, *connect.Request[types.StringValue]) (*connect.Response[v1.Config], error) + GetOperationEvents(context.Context, *connect.Request[emptypb.Empty], *connect.ServerStream[v1.OperationEvent]) error + GetOperations(context.Context, *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) + ListSnapshots(context.Context, *connect.Request[v1.ListSnapshotsRequest]) (*connect.Response[v1.ResticSnapshotList], error) + ListSnapshotFiles(context.Context, *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) + // DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + DoRepoTask(context.Context, *connect.Request[v1.DoRepoTaskRequest]) (*connect.Response[emptypb.Empty], error) + // Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + Forget(context.Context, *connect.Request[v1.ForgetRequest]) (*connect.Response[emptypb.Empty], error) + // Restore schedules a restore operation. + Restore(context.Context, *connect.Request[v1.RestoreSnapshotRequest]) (*connect.Response[emptypb.Empty], error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) + // GetLogs returns the keyed large data for the given operation. + GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error + // RunCommand executes a generic restic command on the repository. + RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) + // GetDownloadURL returns a signed download URL given a forget operation ID. + GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) + // Clears the history of operations + ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. + PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) +} + +// NewBackrestHandler builds an HTTP handler from the service implementation. It returns the path on +// which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (string, http.Handler) { + backrestGetConfigHandler := connect.NewUnaryHandler( + BackrestGetConfigProcedure, + svc.GetConfig, + connect.WithSchema(backrestGetConfigMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestSetConfigHandler := connect.NewUnaryHandler( + BackrestSetConfigProcedure, + svc.SetConfig, + connect.WithSchema(backrestSetConfigMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestCheckRepoExistsHandler := connect.NewUnaryHandler( + BackrestCheckRepoExistsProcedure, + svc.CheckRepoExists, + connect.WithSchema(backrestCheckRepoExistsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestAddRepoHandler := connect.NewUnaryHandler( + BackrestAddRepoProcedure, + svc.AddRepo, + connect.WithSchema(backrestAddRepoMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestRemoveRepoHandler := connect.NewUnaryHandler( + BackrestRemoveRepoProcedure, + svc.RemoveRepo, + connect.WithSchema(backrestRemoveRepoMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestGetOperationEventsHandler := connect.NewServerStreamHandler( + BackrestGetOperationEventsProcedure, + svc.GetOperationEvents, + connect.WithSchema(backrestGetOperationEventsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestGetOperationsHandler := connect.NewUnaryHandler( + BackrestGetOperationsProcedure, + svc.GetOperations, + connect.WithSchema(backrestGetOperationsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestListSnapshotsHandler := connect.NewUnaryHandler( + BackrestListSnapshotsProcedure, + svc.ListSnapshots, + connect.WithSchema(backrestListSnapshotsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestListSnapshotFilesHandler := connect.NewUnaryHandler( + BackrestListSnapshotFilesProcedure, + svc.ListSnapshotFiles, + connect.WithSchema(backrestListSnapshotFilesMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestBackupHandler := connect.NewUnaryHandler( + BackrestBackupProcedure, + svc.Backup, + connect.WithSchema(backrestBackupMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestDoRepoTaskHandler := connect.NewUnaryHandler( + BackrestDoRepoTaskProcedure, + svc.DoRepoTask, + connect.WithSchema(backrestDoRepoTaskMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestForgetHandler := connect.NewUnaryHandler( + BackrestForgetProcedure, + svc.Forget, + connect.WithSchema(backrestForgetMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestRestoreHandler := connect.NewUnaryHandler( + BackrestRestoreProcedure, + svc.Restore, + connect.WithSchema(backrestRestoreMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestCancelHandler := connect.NewUnaryHandler( + BackrestCancelProcedure, + svc.Cancel, + connect.WithSchema(backrestCancelMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestGetLogsHandler := connect.NewServerStreamHandler( + BackrestGetLogsProcedure, + svc.GetLogs, + connect.WithSchema(backrestGetLogsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestRunCommandHandler := connect.NewUnaryHandler( + BackrestRunCommandProcedure, + svc.RunCommand, + connect.WithSchema(backrestRunCommandMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestGetDownloadURLHandler := connect.NewUnaryHandler( + BackrestGetDownloadURLProcedure, + svc.GetDownloadURL, + connect.WithSchema(backrestGetDownloadURLMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestClearHistoryHandler := connect.NewUnaryHandler( + BackrestClearHistoryProcedure, + svc.ClearHistory, + connect.WithSchema(backrestClearHistoryMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestPathAutocompleteHandler := connect.NewUnaryHandler( + BackrestPathAutocompleteProcedure, + svc.PathAutocomplete, + connect.WithSchema(backrestPathAutocompleteMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestGetSummaryDashboardHandler := connect.NewUnaryHandler( + BackrestGetSummaryDashboardProcedure, + svc.GetSummaryDashboard, + connect.WithSchema(backrestGetSummaryDashboardMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/v1.Backrest/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case BackrestGetConfigProcedure: + backrestGetConfigHandler.ServeHTTP(w, r) + case BackrestSetConfigProcedure: + backrestSetConfigHandler.ServeHTTP(w, r) + case BackrestCheckRepoExistsProcedure: + backrestCheckRepoExistsHandler.ServeHTTP(w, r) + case BackrestAddRepoProcedure: + backrestAddRepoHandler.ServeHTTP(w, r) + case BackrestRemoveRepoProcedure: + backrestRemoveRepoHandler.ServeHTTP(w, r) + case BackrestGetOperationEventsProcedure: + backrestGetOperationEventsHandler.ServeHTTP(w, r) + case BackrestGetOperationsProcedure: + backrestGetOperationsHandler.ServeHTTP(w, r) + case BackrestListSnapshotsProcedure: + backrestListSnapshotsHandler.ServeHTTP(w, r) + case BackrestListSnapshotFilesProcedure: + backrestListSnapshotFilesHandler.ServeHTTP(w, r) + case BackrestBackupProcedure: + backrestBackupHandler.ServeHTTP(w, r) + case BackrestDoRepoTaskProcedure: + backrestDoRepoTaskHandler.ServeHTTP(w, r) + case BackrestForgetProcedure: + backrestForgetHandler.ServeHTTP(w, r) + case BackrestRestoreProcedure: + backrestRestoreHandler.ServeHTTP(w, r) + case BackrestCancelProcedure: + backrestCancelHandler.ServeHTTP(w, r) + case BackrestGetLogsProcedure: + backrestGetLogsHandler.ServeHTTP(w, r) + case BackrestRunCommandProcedure: + backrestRunCommandHandler.ServeHTTP(w, r) + case BackrestGetDownloadURLProcedure: + backrestGetDownloadURLHandler.ServeHTTP(w, r) + case BackrestClearHistoryProcedure: + backrestClearHistoryHandler.ServeHTTP(w, r) + case BackrestPathAutocompleteProcedure: + backrestPathAutocompleteHandler.ServeHTTP(w, r) + case BackrestGetSummaryDashboardProcedure: + backrestGetSummaryDashboardHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedBackrestHandler returns CodeUnimplemented from all methods. +type UnimplementedBackrestHandler struct{} + +func (UnimplementedBackrestHandler) GetConfig(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetConfig is not implemented")) +} + +func (UnimplementedBackrestHandler) SetConfig(context.Context, *connect.Request[v1.Config]) (*connect.Response[v1.Config], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.SetConfig is not implemented")) +} + +func (UnimplementedBackrestHandler) CheckRepoExists(context.Context, *connect.Request[v1.Repo]) (*connect.Response[types.BoolValue], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.CheckRepoExists is not implemented")) +} + +func (UnimplementedBackrestHandler) AddRepo(context.Context, *connect.Request[v1.Repo]) (*connect.Response[v1.Config], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.AddRepo is not implemented")) +} + +func (UnimplementedBackrestHandler) RemoveRepo(context.Context, *connect.Request[types.StringValue]) (*connect.Response[v1.Config], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RemoveRepo is not implemented")) +} + +func (UnimplementedBackrestHandler) GetOperationEvents(context.Context, *connect.Request[emptypb.Empty], *connect.ServerStream[v1.OperationEvent]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetOperationEvents is not implemented")) +} + +func (UnimplementedBackrestHandler) GetOperations(context.Context, *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetOperations is not implemented")) +} + +func (UnimplementedBackrestHandler) ListSnapshots(context.Context, *connect.Request[v1.ListSnapshotsRequest]) (*connect.Response[v1.ResticSnapshotList], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.ListSnapshots is not implemented")) +} + +func (UnimplementedBackrestHandler) ListSnapshotFiles(context.Context, *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.ListSnapshotFiles is not implemented")) +} + +func (UnimplementedBackrestHandler) Backup(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.Backup is not implemented")) +} + +func (UnimplementedBackrestHandler) DoRepoTask(context.Context, *connect.Request[v1.DoRepoTaskRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.DoRepoTask is not implemented")) +} + +func (UnimplementedBackrestHandler) Forget(context.Context, *connect.Request[v1.ForgetRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.Forget is not implemented")) +} + +func (UnimplementedBackrestHandler) Restore(context.Context, *connect.Request[v1.RestoreSnapshotRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.Restore is not implemented")) +} + +func (UnimplementedBackrestHandler) Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.Cancel is not implemented")) +} + +func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented")) +} + +func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RunCommand is not implemented")) +} + +func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetDownloadURL is not implemented")) +} + +func (UnimplementedBackrestHandler) ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.ClearHistory is not implemented")) +} + +func (UnimplementedBackrestHandler) PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.PathAutocomplete is not implemented")) +} + +func (UnimplementedBackrestHandler) GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetSummaryDashboard is not implemented")) +} diff --git a/gen/go/v1/v1connect/syncservice.connect.go b/gen/go/v1/v1connect/syncservice.connect.go new file mode 100644 index 000000000..c4821b51e --- /dev/null +++ b/gen/go/v1/v1connect/syncservice.connect.go @@ -0,0 +1,144 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: v1/syncservice.proto + +package v1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // BackrestSyncServiceName is the fully-qualified name of the BackrestSyncService service. + BackrestSyncServiceName = "v1.BackrestSyncService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // BackrestSyncServiceSyncProcedure is the fully-qualified name of the BackrestSyncService's Sync + // RPC. + BackrestSyncServiceSyncProcedure = "/v1.BackrestSyncService/Sync" + // BackrestSyncServiceGetRemoteReposProcedure is the fully-qualified name of the + // BackrestSyncService's GetRemoteRepos RPC. + BackrestSyncServiceGetRemoteReposProcedure = "/v1.BackrestSyncService/GetRemoteRepos" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + backrestSyncServiceServiceDescriptor = v1.File_v1_syncservice_proto.Services().ByName("BackrestSyncService") + backrestSyncServiceSyncMethodDescriptor = backrestSyncServiceServiceDescriptor.Methods().ByName("Sync") + backrestSyncServiceGetRemoteReposMethodDescriptor = backrestSyncServiceServiceDescriptor.Methods().ByName("GetRemoteRepos") +) + +// BackrestSyncServiceClient is a client for the v1.BackrestSyncService service. +type BackrestSyncServiceClient interface { + Sync(context.Context) *connect.BidiStreamForClient[v1.SyncStreamItem, v1.SyncStreamItem] + GetRemoteRepos(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetRemoteReposResponse], error) +} + +// NewBackrestSyncServiceClient constructs a client for the v1.BackrestSyncService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewBackrestSyncServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) BackrestSyncServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &backrestSyncServiceClient{ + sync: connect.NewClient[v1.SyncStreamItem, v1.SyncStreamItem]( + httpClient, + baseURL+BackrestSyncServiceSyncProcedure, + connect.WithSchema(backrestSyncServiceSyncMethodDescriptor), + connect.WithClientOptions(opts...), + ), + getRemoteRepos: connect.NewClient[emptypb.Empty, v1.GetRemoteReposResponse]( + httpClient, + baseURL+BackrestSyncServiceGetRemoteReposProcedure, + connect.WithSchema(backrestSyncServiceGetRemoteReposMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// backrestSyncServiceClient implements BackrestSyncServiceClient. +type backrestSyncServiceClient struct { + sync *connect.Client[v1.SyncStreamItem, v1.SyncStreamItem] + getRemoteRepos *connect.Client[emptypb.Empty, v1.GetRemoteReposResponse] +} + +// Sync calls v1.BackrestSyncService.Sync. +func (c *backrestSyncServiceClient) Sync(ctx context.Context) *connect.BidiStreamForClient[v1.SyncStreamItem, v1.SyncStreamItem] { + return c.sync.CallBidiStream(ctx) +} + +// GetRemoteRepos calls v1.BackrestSyncService.GetRemoteRepos. +func (c *backrestSyncServiceClient) GetRemoteRepos(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetRemoteReposResponse], error) { + return c.getRemoteRepos.CallUnary(ctx, req) +} + +// BackrestSyncServiceHandler is an implementation of the v1.BackrestSyncService service. +type BackrestSyncServiceHandler interface { + Sync(context.Context, *connect.BidiStream[v1.SyncStreamItem, v1.SyncStreamItem]) error + GetRemoteRepos(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetRemoteReposResponse], error) +} + +// NewBackrestSyncServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewBackrestSyncServiceHandler(svc BackrestSyncServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + backrestSyncServiceSyncHandler := connect.NewBidiStreamHandler( + BackrestSyncServiceSyncProcedure, + svc.Sync, + connect.WithSchema(backrestSyncServiceSyncMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + backrestSyncServiceGetRemoteReposHandler := connect.NewUnaryHandler( + BackrestSyncServiceGetRemoteReposProcedure, + svc.GetRemoteRepos, + connect.WithSchema(backrestSyncServiceGetRemoteReposMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/v1.BackrestSyncService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case BackrestSyncServiceSyncProcedure: + backrestSyncServiceSyncHandler.ServeHTTP(w, r) + case BackrestSyncServiceGetRemoteReposProcedure: + backrestSyncServiceGetRemoteReposHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedBackrestSyncServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedBackrestSyncServiceHandler struct{} + +func (UnimplementedBackrestSyncServiceHandler) Sync(context.Context, *connect.BidiStream[v1.SyncStreamItem, v1.SyncStreamItem]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("v1.BackrestSyncService.Sync is not implemented")) +} + +func (UnimplementedBackrestSyncServiceHandler) GetRemoteRepos(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetRemoteReposResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.BackrestSyncService.GetRemoteRepos is not implemented")) +} diff --git a/gen/ts/fetch.pb.ts b/gen/ts/fetch.pb.ts deleted file mode 100644 index 827363613..000000000 --- a/gen/ts/fetch.pb.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -/* -* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY -*/ - -/** - * base64 encoder and decoder - * Copied and adapted from https://github.com/protobufjs/protobuf.js/blob/master/lib/base64/index.js - */ -// Base64 encoding table -const b64 = new Array(64); - -// Base64 decoding table -const s64 = new Array(123); - -// 65..90, 97..122, 48..57, 43, 47 -for (let i = 0; i < 64;) - s64[b64[i] = i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i - 59 | 43] = i++; - -export function b64Encode(buffer: Uint8Array, start: number, end: number): string { - let parts: string[] = null; - const chunk = []; - let i = 0, // output index - j = 0, // goto index - t; // temporary - while (start < end) { - const b = buffer[start++]; - switch (j) { - case 0: - chunk[i++] = b64[b >> 2]; - t = (b & 3) << 4; - j = 1; - break; - case 1: - chunk[i++] = b64[t | b >> 4]; - t = (b & 15) << 2; - j = 2; - break; - case 2: - chunk[i++] = b64[t | b >> 6]; - chunk[i++] = b64[b & 63]; - j = 0; - break; - } - if (i > 8191) { - (parts || (parts = [])).push(String.fromCharCode.apply(String, chunk)); - i = 0; - } - } - if (j) { - chunk[i++] = b64[t]; - chunk[i++] = 61; - if (j === 1) - chunk[i++] = 61; - } - if (parts) { - if (i) - parts.push(String.fromCharCode.apply(String, chunk.slice(0, i))); - return parts.join(""); - } - return String.fromCharCode.apply(String, chunk.slice(0, i)); -} - -const invalidEncoding = "invalid encoding"; - -export function b64Decode(s: string): Uint8Array { - const buffer = []; - let offset = 0; - let j = 0, // goto index - t; // temporary - for (let i = 0; i < s.length;) { - let c = s.charCodeAt(i++); - if (c === 61 && j > 1) - break; - if ((c = s64[c]) === undefined) - throw Error(invalidEncoding); - switch (j) { - case 0: - t = c; - j = 1; - break; - case 1: - buffer[offset++] = t << 2 | (c & 48) >> 4; - t = c; - j = 2; - break; - case 2: - buffer[offset++] = (t & 15) << 4 | (c & 60) >> 2; - t = c; - j = 3; - break; - case 3: - buffer[offset++] = (t & 3) << 6 | c; - j = 0; - break; - } - } - if (j === 1) - throw Error(invalidEncoding); - return new Uint8Array(buffer); -} - -function b64Test(s: string): boolean { - return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s); -} - -export interface InitReq extends RequestInit { - pathPrefix?: string -} - -export function replacer(key: any, value: any): any { - if(value && value.constructor === Uint8Array) { - return b64Encode(value, 0, value.length); - } - - return value; -} - -export function fetchReq(path: string, init?: InitReq): Promise { - const {pathPrefix, ...req} = init || {} - - const url = pathPrefix ? `${pathPrefix}${path}` : path - - return fetch(url, req).then(r => r.json().then((body: O) => { - if (!r.ok) { throw body; } - return body; - })) as Promise -} - -// NotifyStreamEntityArrival is a callback that will be called on streaming entity arrival -export type NotifyStreamEntityArrival = (resp: T) => void - -/** - * fetchStreamingRequest is able to handle grpc-gateway server side streaming call - * it takes NotifyStreamEntityArrival that lets users respond to entity arrival during the call - * all entities will be returned as an array after the call finishes. - **/ -export async function fetchStreamingRequest(path: string, callback?: NotifyStreamEntityArrival, init?: InitReq) { - const {pathPrefix, ...req} = init || {} - const url = pathPrefix ?`${pathPrefix}${path}` : path - const result = await fetch(url, req) - // needs to use the .ok to check the status of HTTP status code - // http other than 200 will not throw an error, instead the .ok will become false. - // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch# - if (!result.ok) { - const resp = await result.json() - const errMsg = resp.error && resp.error.message ? resp.error.message : "" - throw new Error(errMsg) - } - - if (!result.body) { - throw new Error("response doesnt have a body") - } - - await result.body - .pipeThrough(new TextDecoderStream()) - .pipeThrough(getNewLineDelimitedJSONDecodingStream()) - .pipeTo(getNotifyEntityArrivalSink((e: R) => { - if (callback) { - callback(e) - } - })) - - // wait for the streaming to finish and return the success respond - return -} - -/** - * JSONStringStreamController represents the transform controller that's able to transform the incoming - * new line delimited json content stream into entities and able to push the entity to the down stream - */ -interface JSONStringStreamController extends TransformStreamDefaultController { - buf?: string - pos?: number - enqueue: (s: T) => void -} - -/** - * getNewLineDelimitedJSONDecodingStream returns a TransformStream that's able to handle new line delimited json stream content into parsed entities - */ -function getNewLineDelimitedJSONDecodingStream(): TransformStream { - return new TransformStream({ - start(controller: JSONStringStreamController) { - controller.buf = '' - controller.pos = 0 - }, - - transform(chunk: string, controller: JSONStringStreamController) { - if (controller.buf === undefined) { - controller.buf = '' - } - if (controller.pos === undefined) { - controller.pos = 0 - } - controller.buf += chunk - while (controller.pos < controller.buf.length) { - if (controller.buf[controller.pos] === '\n') { - const line = controller.buf.substring(0, controller.pos) - const response = JSON.parse(line) - controller.enqueue(response.result) - controller.buf = controller.buf.substring(controller.pos + 1) - controller.pos = 0 - } else { - ++controller.pos - } - } - } - }) - -} - -/** - * getNotifyEntityArrivalSink takes the NotifyStreamEntityArrival callback and return - * a sink that will call the callback on entity arrival - * @param notifyCallback - */ -function getNotifyEntityArrivalSink(notifyCallback: NotifyStreamEntityArrival) { - return new WritableStream({ - write(entity: T) { - notifyCallback(entity) - } - }) -} - -type Primitive = string | boolean | number; -type RequestPayload = Record; -type FlattenedRequestPayload = Record>; - -/** - * Checks if given value is a plain object - * Logic copied and adapted from below source: - * https://github.com/char0n/ramda-adjunct/blob/master/src/isPlainObj.js - * @param {unknown} value - * @return {boolean} - */ -function isPlainObject(value: unknown): boolean { - const isObject = - Object.prototype.toString.call(value).slice(8, -1) === "Object"; - const isObjLike = value !== null && isObject; - - if (!isObjLike || !isObject) { - return false; - } - - const proto = Object.getPrototypeOf(value); - - const hasObjectConstructor = - typeof proto === "object" && - proto.constructor === Object.prototype.constructor; - - return hasObjectConstructor; -} - -/** - * Checks if given value is of a primitive type - * @param {unknown} value - * @return {boolean} - */ -function isPrimitive(value: unknown): boolean { - return ["string", "number", "boolean"].some(t => typeof value === t); -} - -/** - * Checks if given primitive is zero-value - * @param {Primitive} value - * @return {boolean} - */ -function isZeroValuePrimitive(value: Primitive): boolean { - return value === false || value === 0 || value === ""; -} - -/** - * Flattens a deeply nested request payload and returns an object - * with only primitive values and non-empty array of primitive values - * as per https://github.com/googleapis/googleapis/blob/master/google/api/http.proto - * @param {RequestPayload} requestPayload - * @param {String} path - * @return {FlattenedRequestPayload>} - */ -function flattenRequestPayload( - requestPayload: T, - path: string = "" -): FlattenedRequestPayload { - return Object.keys(requestPayload).reduce( - (acc: T, key: string): T => { - const value = requestPayload[key]; - const newPath = path ? [path, key].join(".") : key; - - const isNonEmptyPrimitiveArray = - Array.isArray(value) && - value.every(v => isPrimitive(v)) && - value.length > 0; - - const isNonZeroValuePrimitive = - isPrimitive(value) && !isZeroValuePrimitive(value as Primitive); - - let objectToMerge = {}; - - if (isPlainObject(value)) { - objectToMerge = flattenRequestPayload(value as RequestPayload, newPath); - } else if (isNonZeroValuePrimitive || isNonEmptyPrimitiveArray) { - objectToMerge = { [newPath]: value }; - } - - return { ...acc, ...objectToMerge }; - }, - {} as T - ) as FlattenedRequestPayload; -} - -/** - * Renders a deeply nested request payload into a string of URL search - * parameters by first flattening the request payload and then removing keys - * which are already present in the URL path. - * @param {RequestPayload} requestPayload - * @param {string[]} urlPathParams - * @return {string} - */ -export function renderURLSearchParams( - requestPayload: T, - urlPathParams: string[] = [] -): string { - const flattenedRequestPayload = flattenRequestPayload(requestPayload); - - const urlSearchParams = Object.keys(flattenedRequestPayload).reduce( - (acc: string[][], key: string): string[][] => { - // key should not be present in the url path as a parameter - const value = flattenedRequestPayload[key]; - if (urlPathParams.find(f => f === key)) { - return acc; - } - return Array.isArray(value) - ? [...acc, ...value.map(m => [key, m.toString()])] - : (acc = [...acc, [key, value.toString()]]); - }, - [] as string[][] - ); - - return new URLSearchParams(urlSearchParams).toString(); -} \ No newline at end of file diff --git a/gen/ts/google/api/annotations.pb.ts b/gen/ts/google/api/annotations.pb.ts deleted file mode 100644 index 56004c9f9..000000000 --- a/gen/ts/google/api/annotations.pb.ts +++ /dev/null @@ -1 +0,0 @@ -export default {} \ No newline at end of file diff --git a/gen/ts/google/api/http.pb.ts b/gen/ts/google/api/http.pb.ts deleted file mode 100644 index 9fe73dff2..000000000 --- a/gen/ts/google/api/http.pb.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -/* -* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY -*/ - -type Absent = { [k in Exclude]?: undefined }; -type OneOf = - | { [k in keyof T]?: undefined } - | ( - keyof T extends infer K ? - (K extends string & keyof T ? { [k in K]: T[K] } & Absent - : never) - : never); -export type Http = { - rules?: HttpRule[] - fullyDecodeReservedExpansion?: boolean -} - - -type BaseHttpRule = { - selector?: string - body?: string - responseBody?: string - additionalBindings?: HttpRule[] -} - -export type HttpRule = BaseHttpRule - & OneOf<{ get: string; put: string; post: string; delete: string; patch: string; custom: CustomHttpPattern }> - -export type CustomHttpPattern = { - kind?: string - path?: string -} \ No newline at end of file diff --git a/gen/ts/v1/config.pb.ts b/gen/ts/v1/config.pb.ts deleted file mode 100644 index 1bbadee2e..000000000 --- a/gen/ts/v1/config.pb.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -/* -* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY -*/ -export type Config = { - version?: number - logDir?: string - repos?: Repo[] - plans?: Plan[] -} - -export type Repo = { - id?: string - uri?: string - password?: string - env?: string[] -} - -export type Plan = { - id?: string - repo?: string - repoPath?: string - paths?: string[] -} \ No newline at end of file diff --git a/gen/ts/v1/events.pb.ts b/gen/ts/v1/events.pb.ts deleted file mode 100644 index 62dfd6c62..000000000 --- a/gen/ts/v1/events.pb.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -/* -* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY -*/ - -type Absent = { [k in Exclude]?: undefined }; -type OneOf = - | { [k in keyof T]?: undefined } - | ( - keyof T extends infer K ? - (K extends string & keyof T ? { [k in K]: T[K] } & Absent - : never) - : never); - -export enum Status { - UNKNOWN = "UNKNOWN", - IN_PROGRESS = "IN_PROGRESS", - SUCCESS = "SUCCESS", - FAILED = "FAILED", -} - - -type BaseEvent = { - timestamp?: string -} - -export type Event = BaseEvent - & OneOf<{ log: LogEvent; backupStatusChange: BackupStatusEvent }> - -export type LogEvent = { - message?: string -} - -export type BackupStatusEvent = { - plan?: string - status?: Status - percent?: number -} \ No newline at end of file diff --git a/gen/ts/v1/service.pb.ts b/gen/ts/v1/service.pb.ts deleted file mode 100644 index fda7ca613..000000000 --- a/gen/ts/v1/service.pb.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -/* -* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY -*/ - -import * as fm from "../fetch.pb" -import * as GoogleProtobufEmpty from "../google/protobuf/empty.pb" -import * as V1Config from "./config.pb" -import * as V1Events from "./events.pb" -export class ResticUI { - static GetConfig(req: GoogleProtobufEmpty.Empty, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/config?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) - } - static SetConfig(req: V1Config.Config, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/config`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) - } - static GetEvents(req: GoogleProtobufEmpty.Empty, entityNotifier?: fm.NotifyStreamEntityArrival, initReq?: fm.InitReq): Promise { - return fm.fetchStreamingRequest(`/v1/events?${fm.renderURLSearchParams(req, [])}`, entityNotifier, {...initReq, method: "GET"}) - } -} \ No newline at end of file diff --git a/go.mod b/go.mod index f2f60fcda..9f08d5b02 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,87 @@ -module github.com/garethgeorge/resticui +module github.com/garethgeorge/backrest -go 1.21.3 +go 1.24 + +replace ( + modernc.org/libc => modernc.org/libc v1.55.3 + modernc.org/mathutil => modernc.org/mathutil v1.6.0 + modernc.org/memory => modernc.org/memory v1.8.0 + modernc.org/sqlite => modernc.org/sqlite v1.33.1 + zombiezen.com/go/sqlite => zombiezen.com/go/sqlite v1.4.0 +) require ( - github.com/google/renameio v1.0.1 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 + al.essio.dev/pkg/shellescape v1.5.1 + connectrpc.com/connect v1.17.0 + github.com/containrrr/shoutrrr v0.8.0 + github.com/djherbis/buffer v1.2.0 + github.com/djherbis/nio/v3 v3.0.1 + github.com/getlantern/systray v1.2.2 + github.com/gitploy-io/cronexpr v0.2.2 + github.com/gofrs/flock v0.12.1 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/go-cmp v0.6.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 - go.uber.org/zap v1.26.0 - golang.org/x/sync v0.5.0 - google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 - google.golang.org/grpc v1.59.0 - google.golang.org/protobuf v1.31.0 - gopkg.in/yaml.v3 v3.0.1 + github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb + github.com/mattn/go-colorable v0.1.13 + github.com/natefinch/atomic v1.0.1 + github.com/ncruces/zenity v0.10.14 + github.com/prometheus/client_golang v1.20.5 + go.etcd.io/bbolt v1.3.11 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.29.0 + golang.org/x/net v0.31.0 + golang.org/x/sync v0.9.0 + google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f + google.golang.org/grpc v1.68.0 + google.golang.org/protobuf v1.35.2 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + zombiezen.com/go/sqlite v1.4.0 ) require ( - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/akavel/rsrc v0.10.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dchest/jsmin v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect + github.com/getlantern/errors v1.0.4 // indirect + github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect + github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect + github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect + github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-stack/stack v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/vine-io/vine v1.6.16 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/josephspurrier/goversioninfo v1.4.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + golang.org/x/image v0.22.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + modernc.org/libc v1.61.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index 65d95da88..dc6a46d84 100644 --- a/go.sum +++ b/go.sum @@ -1,140 +1,254 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= -github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= +connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= +github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/jsmin v1.0.0 h1:Y2hWXmGZiRxtl+VcTksyucgTlYxnhPzTozCwx9gy9zI= +github.com/dchest/jsmin v1.0.0/go.mod h1:AVBIund7Mr7lKXT70hKT2YgL3XEXUaUk5iw9DZ8b0Uc= +github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= +github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= +github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= +github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= +github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= +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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= +github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= +github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0= +github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= +github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0= +github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= +github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU= +github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= +github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE= +github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag= +github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y= +github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= +github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= +github.com/gitploy-io/cronexpr v0.2.2 h1:Au+wK6FqmOLAF7AkW6q4gnrNXTe3rEW97XFZ4chy0xs= +github.com/gitploy-io/cronexpr v0.2.2/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/vine-io/vine v1.6.16 h1:KZ1sqxjdeCNJ+rOVKAH+hysQeXqzSJ7nrJPcMtfCKbs= -github.com/vine-io/vine v1.6.16/go.mod h1:Ur2SyDUlnwvanr8uc1vx8fa+Eq06Zlpr6rNozKLYyVs= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM= +github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1t4uSsWjIonrqY= +github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/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/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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/zenity v0.10.14 h1:OBFl7qfXcvsdo1NUEGxTlZvAakgWMqz9nG38TuiaGLI= +github.com/ncruces/zenity v0.10.14/go.mod h1:ZBW7uVe/Di3IcRYH0Br8X59pi+O6EPnNIOU66YHpOO4= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= +github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= +zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= diff --git a/install.sh b/install.sh new file mode 100755 index 000000000..4ae511298 --- /dev/null +++ b/install.sh @@ -0,0 +1,108 @@ +#! /bin/bash + +cd "$(dirname "$0")" # cd to the directory of this script + +install_or_update_unix() { + if systemctl is-active --quiet backrest; then + sudo systemctl stop backrest + echo "Paused backrest for update" + fi + install_unix +} + +install_unix() { + echo "Installing backrest to /usr/local/bin" + sudo mkdir -p /usr/local/bin + + sudo cp $(ls -1 backrest | head -n 1) /usr/local/bin +} + +create_systemd_service() { + if [ ! -d /etc/systemd/system ]; then + echo "Systemd not found. This script is only for systemd based systems." + exit 1 + fi + + if [ -f /etc/systemd/system/backrest.service ]; then + echo "Systemd unit already exists. Skipping creation." + return 0 + fi + + echo "Creating systemd service at /etc/systemd/system/backrest.service" + + sudo tee /etc/systemd/system/backrest.service > /dev/null <<- EOM +[Unit] +Description=Backrest Service +After=network.target + +[Service] +Type=simple +User=$(whoami) +Group=$(whoami) +ExecStart=/usr/local/bin/backrest +Environment="BACKREST_PORT=127.0.0.1:9898" + +[Install] +WantedBy=multi-user.target +EOM + + echo "Reloading systemd daemon" + sudo systemctl daemon-reload +} + +create_launchd_plist() { + echo "Creating launchd plist at /Library/LaunchAgents/com.backrest.plist" + + sudo tee /Library/LaunchAgents/com.backrest.plist > /dev/null <<- EOM + + + + + Label + com.backrest + ProgramArguments + + /usr/local/bin/backrest + + KeepAlive + + EnvironmentVariables + + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + BACKREST_PORT + 127.0.0.1:9898 + + + +EOM +} + +enable_launchd_plist() { + echo "Trying to unload any previous version of com.backrest.plist" + launchctl unload /Library/LaunchAgents/com.backrest.plist || true + echo "Loading com.backrest.plist" + launchctl load -w /Library/LaunchAgents/com.backrest.plist +} + +OS=$(uname -s) +if [ "$OS" = "Darwin" ]; then + echo "Installing on Darwin" + install_unix + create_launchd_plist + enable_launchd_plist + sudo xattr -d com.apple.quarantine /usr/local/bin/backrest # remove quarantine flag +elif [ "$OS" = "Linux" ]; then + echo "Installing on Linux" + install_or_update_unix + create_systemd_service + echo "Enabling systemd service backrest.service" + sudo systemctl enable backrest + sudo systemctl start backrest +else + echo "Unknown OS: $OS. This script only supports Darwin and Linux." + exit 1 +fi + +echo "Logs are available at ~/.local/share/backrest/processlogs/backrest.log" +echo "Access backrest WebUI at http://localhost:9898" diff --git a/internal/api/api.go b/internal/api/api.go deleted file mode 100644 index 14c14140c..000000000 --- a/internal/api/api.go +++ /dev/null @@ -1,64 +0,0 @@ -package api - -import ( - "context" - "fmt" - "net" - "net/http" - "os" - "path/filepath" - - v1 "github.com/garethgeorge/resticui/gen/go/v1" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -func serveGRPC(ctx context.Context, socket string) error { - lis, err := net.Listen("unix", socket) - if err != nil { - return fmt.Errorf("failed to listen: %w", err) - } - grpcServer := grpc.NewServer() - v1.RegisterResticUIServer(grpcServer, &server{}) - go func() { - <-ctx.Done() - grpcServer.GracefulStop() - }() - err = grpcServer.Serve(lis) - if err != nil { - return fmt.Errorf("grpc serving error: %w", err) - } - return nil -} - -func serveHTTPHandlers(ctx context.Context, mux *runtime.ServeMux) error { - tmpDir, err := os.MkdirTemp("", "resticui") - if err != nil { - return fmt.Errorf("failed to create temp dir for unix domain socket: %w", err) - } - defer func() { - os.RemoveAll(tmpDir) - }() - - socket := filepath.Join(tmpDir, "resticui.sock") - - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - err = v1.RegisterResticUIHandlerFromEndpoint(ctx, mux, fmt.Sprintf("unix:%v", socket), opts) - if err != nil { - return fmt.Errorf("failed to register gateway: %w", err) - } - - if err := serveGRPC(ctx, socket); err != nil { - return err - } - - return nil -} - -// Handler returns an http.Handler serving the API, cancel the context to cleanly shut down the server. -func ServeAPI(ctx context.Context, mux *http.ServeMux) error { - apiMux := runtime.NewServeMux() - mux.Handle("/api/", http.StripPrefix("/api", apiMux)) - return serveHTTPHandlers(ctx, apiMux) -} \ No newline at end of file diff --git a/internal/api/authenticationhandler.go b/internal/api/authenticationhandler.go new file mode 100644 index 000000000..a29b0de16 --- /dev/null +++ b/internal/api/authenticationhandler.go @@ -0,0 +1,51 @@ +package api + +import ( + "context" + + "connectrpc.com/connect" + "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + "github.com/garethgeorge/backrest/internal/auth" + "go.uber.org/zap" +) + +type AuthenticationHandler struct { + // v1connect.UnimplementedAuthenticationHandler + authenticator *auth.Authenticator +} + +var _ v1connect.AuthenticationHandler = &AuthenticationHandler{} + +func NewAuthenticationHandler(authenticator *auth.Authenticator) *AuthenticationHandler { + return &AuthenticationHandler{ + authenticator: authenticator, + } +} + +func (s *AuthenticationHandler) Login(ctx context.Context, req *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) { + zap.L().Debug("login request", zap.String("username", req.Msg.Username)) + user, err := s.authenticator.Login(req.Msg.Username, req.Msg.Password) + if err != nil { + zap.L().Warn("failed login attempt", zap.Error(err)) + return nil, connect.NewError(connect.CodeUnauthenticated, auth.ErrInvalidPassword) + } + + token, err := s.authenticator.CreateJWT(user) + if err != nil { + return nil, err + } + + return connect.NewResponse(&v1.LoginResponse{ + Token: token, + }), nil +} + +func (s *AuthenticationHandler) HashPassword(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[types.StringValue], error) { + hash, err := auth.CreatePassword(req.Msg.Value) + if err != nil { + return nil, err + } + return connect.NewResponse(&types.StringValue{Value: hash}), nil +} diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go new file mode 100644 index 000000000..4ea2d5c66 --- /dev/null +++ b/internal/api/backresthandler.go @@ -0,0 +1,901 @@ +package api + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path" + "slices" + "strings" + "sync" + "time" + + "connectrpc.com/connect" + "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + syncapi "github.com/garethgeorge/backrest/internal/api/syncapi" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/env" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/garethgeorge/backrest/internal/protoutil" + "github.com/garethgeorge/backrest/internal/resticinstaller" + "github.com/garethgeorge/backrest/pkg/restic" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" +) + +type BackrestHandler struct { + v1connect.UnimplementedBackrestHandler + config config.ConfigStore + orchestrator *orchestrator.Orchestrator + oplog *oplog.OpLog + logStore *logstore.LogStore + remoteConfigStore syncapi.RemoteConfigStore +} + +var _ v1connect.BackrestHandler = &BackrestHandler{} + +func NewBackrestHandler(config config.ConfigStore, remoteConfigStore syncapi.RemoteConfigStore, orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog, logStore *logstore.LogStore) *BackrestHandler { + s := &BackrestHandler{ + config: config, + orchestrator: orchestrator, + oplog: oplog, + logStore: logStore, + remoteConfigStore: remoteConfigStore, + } + + return s +} + +// GetConfig implements GET /v1/config +func (s *BackrestHandler) GetConfig(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) { + config, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + return connect.NewResponse(config), nil +} + +// SetConfig implements POST /v1/config +func (s *BackrestHandler) SetConfig(ctx context.Context, req *connect.Request[v1.Config]) (*connect.Response[v1.Config], error) { + existing, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to check current config: %w", err) + } + + // Compare and increment modno + if existing.Modno != req.Msg.Modno { + return nil, errors.New("config modno mismatch, reload and try again") + } + + if err := config.ValidateConfig(req.Msg); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + req.Msg.Modno += 1 + + if err := s.config.Update(req.Msg); err != nil { + return nil, fmt.Errorf("failed to update config: %w", err) + } + + newConfig, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get newly set config: %w", err) + } + return connect.NewResponse(newConfig), nil +} + +func (s *BackrestHandler) CheckRepoExists(ctx context.Context, req *connect.Request[v1.Repo]) (*connect.Response[types.BoolValue], error) { + c, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + c = proto.Clone(c).(*v1.Config) + if idx := slices.IndexFunc(c.Repos, func(r *v1.Repo) bool { return r.Id == req.Msg.Id }); idx != -1 { + c.Repos[idx] = req.Msg + } else { + c.Repos = append(c.Repos, req.Msg) + } + + if req.Msg.Guid == "" { + req.Msg.Guid = cryptoutil.MustRandomID(cryptoutil.DefaultIDBits) + } + + if err := config.ValidateConfig(c); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + bin, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + return nil, fmt.Errorf("failed to find or install restic binary: %w", err) + } + + r, err := repo.NewRepoOrchestrator(c, req.Msg, bin) + if err != nil { + return nil, fmt.Errorf("failed to configure repo: %w", err) + } + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + if err := r.Exists(ctx); err != nil { + zap.S().Debugf("repo %q exists or not: %v", req.Msg.Id, err) + if errors.Is(err, restic.ErrRepoNotFound) { + zap.S().Debugf("repo %q does not exist", req.Msg.Id) + return connect.NewResponse(&types.BoolValue{Value: false}), nil + } + return nil, err + } + return connect.NewResponse(&types.BoolValue{Value: true}), nil +} + +// AddRepo implements POST /v1/config/repo, it includes validation that the repo can be initialized. +func (s *BackrestHandler) AddRepo(ctx context.Context, req *connect.Request[v1.Repo]) (*connect.Response[v1.Config], error) { + c, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + newRepo := req.Msg + + // Deep copy the configuration + c = proto.Clone(c).(*v1.Config) + + // Add or implicit update the repo + var oldRepo *v1.Repo + if idx := slices.IndexFunc(c.Repos, func(r *v1.Repo) bool { return r.Id == newRepo.Id }); idx != -1 { + oldRepo = c.Repos[idx] + c.Repos[idx] = newRepo + } else { + c.Repos = append(c.Repos, newRepo) + } + + // Ensure the Repo GUID is set to the correct value. + // This is derived from 'restic cat config' for local repos. + // For remote repos, the GUID is derived from the remote config's value for the repo. + if !syncapi.IsBackrestRemoteRepoURI(newRepo.Uri) { + bin, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + return nil, fmt.Errorf("failed to find or install restic binary: %w", err) + } + + r, err := repo.NewRepoOrchestrator(c, newRepo, bin) + if err != nil { + return nil, fmt.Errorf("failed to configure repo: %w", err) + } + + if err := r.Init(ctx); err != nil { + return nil, fmt.Errorf("failed to init repo: %w", err) + } + + guid, err := r.RepoGUID() + zap.S().Debugf("GUID for repo %q is %q from restic", newRepo.Id, guid) + if err != nil { + return nil, fmt.Errorf("failed to get repo config: %w", err) + } + + newRepo.Guid = guid + } else { + // It's a remote repo, let's find the configuration and guid for it. + instanceID, err := syncapi.InstanceForBackrestURI(newRepo.Uri) + if err != nil { + return nil, fmt.Errorf("failed to parse remote repo URI: %w", err) + } + + // fetch the remote config + remoteRepo, err := syncapi.GetRepoConfig(s.remoteConfigStore, instanceID, newRepo.Guid) + if err != nil { + return nil, fmt.Errorf("failed to get remote repo config: %w", err) + } + + // set the GUID from the remote config. + newRepo.Guid = remoteRepo.Guid + if newRepo.Guid == "" { + return nil, fmt.Errorf("GUID not found for repo %q", newRepo.Id) + } + } + + if err := config.ValidateConfig(c); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + zap.L().Debug("updating config", zap.Int32("version", c.Version)) + if err := s.config.Update(c); err != nil { + return nil, fmt.Errorf("failed to update config: %w", err) + } + + // If the GUID has changed, and we just successfully updated the config in storage, then we need to migrate the oplog. + if oldRepo != nil && newRepo.Guid != oldRepo.Guid { + migratedCount := 0 + + q := oplog.Query{}. + SetInstanceID(c.Instance) + // we use RepoID here to _ensure_ we consolidate all operations to the most recent GUID. + // this provides some resiliancy in the case of a previous partial update. + q.DeprecatedRepoID = &oldRepo.Id + if err := s.oplog.Transform(q, func(op *v1.Operation) (*v1.Operation, error) { + op.RepoGuid = newRepo.Guid + migratedCount++ + return op, nil + }); err != nil { + return nil, fmt.Errorf("failed to get operations for repo: %w", err) + } + + zap.S().Infof("updated GUID for repo %q from %q to %q, migrated %d operations to reference the new GUID", newRepo.Id, oldRepo.Guid, newRepo.Guid, migratedCount) + } + + // index snapshots for the newly added repository. + zap.L().Debug("scheduling index snapshots task") + s.orchestrator.ScheduleTask(tasks.NewOneoffIndexSnapshotsTask(newRepo, time.Now()), tasks.TaskPriorityInteractive+tasks.TaskPriorityIndexSnapshots) + + zap.L().Debug("done add repo") + return connect.NewResponse(c), nil +} + +func (s *BackrestHandler) RemoveRepo(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[v1.Config], error) { + cfg, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + // Remove the repo from the configuration + cfg.Repos = slices.DeleteFunc(cfg.Repos, func(r *v1.Repo) bool { + return r.Id == req.Msg.Value + }) + if err := s.config.Update(cfg); err != nil { + return nil, fmt.Errorf("failed to update config: %w", err) + } + + // Query for all operations for the repo + q := oplog.Query{}. + SetInstanceID(cfg.Instance) + q.DeprecatedRepoID = &req.Msg.Value + var opIDs []int64 + if err := s.oplog.Query(q, func(op *v1.Operation) error { + opIDs = append(opIDs, op.Id) + return nil + }); err != nil { + return nil, fmt.Errorf("failed to get operations for repo: %w", err) + } + + // Delete operations referencing the repo from the oplog in batches + for len(opIDs) > 0 { + batchSize := 256 + if batchSize > len(opIDs) { + batchSize = len(opIDs) + } + if err := s.oplog.Delete(opIDs[:batchSize]...); err != nil { + return nil, fmt.Errorf("failed to delete operations: %w", err) + } + opIDs = opIDs[batchSize:] + } + + return connect.NewResponse(cfg), nil +} + +// ListSnapshots implements POST /v1/snapshots +func (s *BackrestHandler) ListSnapshots(ctx context.Context, req *connect.Request[v1.ListSnapshotsRequest]) (*connect.Response[v1.ResticSnapshotList], error) { + query := req.Msg + repo, err := s.orchestrator.GetRepoOrchestrator(query.RepoId) + if err != nil { + return nil, fmt.Errorf("failed to get repo: %w", err) + } + + var snapshots []*restic.Snapshot + if query.PlanId != "" { + var plan *v1.Plan + plan, err = s.orchestrator.GetPlan(query.PlanId) + if err != nil { + return nil, fmt.Errorf("failed to get plan %q: %w", query.PlanId, err) + } + snapshots, err = repo.SnapshotsForPlan(ctx, plan) + } else { + snapshots, err = repo.Snapshots(ctx) + } + + if err != nil { + return nil, fmt.Errorf("failed to list snapshots: %w", err) + } + + // Transform the snapshots and return them. + var rs []*v1.ResticSnapshot + for _, snapshot := range snapshots { + rs = append(rs, protoutil.SnapshotToProto(snapshot)) + } + + return connect.NewResponse(&v1.ResticSnapshotList{ + Snapshots: rs, + }), nil +} + +func (s *BackrestHandler) ListSnapshotFiles(ctx context.Context, req *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) { + query := req.Msg + repo, err := s.orchestrator.GetRepoOrchestrator(query.RepoId) + if err != nil { + return nil, fmt.Errorf("failed to get repo: %w", err) + } + + entries, err := repo.ListSnapshotFiles(ctx, query.SnapshotId, query.Path) + if err != nil { + return nil, fmt.Errorf("failed to list snapshot files: %w", err) + } + + return connect.NewResponse(&v1.ListSnapshotFilesResponse{ + Path: query.Path, + Entries: entries, + }), nil +} + +// GetOperationEvents implements GET /v1/events/operations +func (s *BackrestHandler) GetOperationEvents(ctx context.Context, req *connect.Request[emptypb.Empty], resp *connect.ServerStream[v1.OperationEvent]) error { + errChan := make(chan error, 1) + events := make(chan *v1.OperationEvent, 100) + + timer := time.NewTicker(60 * time.Second) + defer timer.Stop() + + callback := func(ops []*v1.Operation, eventType oplog.OperationEvent) { + var event *v1.OperationEvent + switch eventType { + case oplog.OPERATION_ADDED: + event = &v1.OperationEvent{ + Event: &v1.OperationEvent_CreatedOperations{ + CreatedOperations: &v1.OperationList{ + Operations: ops, + }, + }, + } + case oplog.OPERATION_UPDATED: + event = &v1.OperationEvent{ + Event: &v1.OperationEvent_UpdatedOperations{ + UpdatedOperations: &v1.OperationList{ + Operations: ops, + }, + }, + } + case oplog.OPERATION_DELETED: + ids := make([]int64, len(ops)) + for i, o := range ops { + ids[i] = o.Id + } + + event = &v1.OperationEvent{ + Event: &v1.OperationEvent_DeletedOperations{ + DeletedOperations: &types.Int64List{ + Values: ids, + }, + }, + } + default: + zap.L().Error("Unknown event type") + } + + select { + case events <- event: + default: + select { + case errChan <- errors.New("event buffer overflow, closing stream for client retry and catchup"): + default: + } + } + } + + s.oplog.Subscribe(oplog.SelectAll, &callback) + defer func() { + if err := s.oplog.Unsubscribe(&callback); err != nil { + zap.L().Error("failed to unsubscribe from oplog", zap.Error(err)) + } + }() + + for { + select { + case <-timer.C: + if err := resp.Send(&v1.OperationEvent{ + Event: &v1.OperationEvent_KeepAlive{}, + }); err != nil { + return err + } + case err := <-errChan: + return err + case <-ctx.Done(): + return nil + case event := <-events: + if err := resp.Send(event); err != nil { + return err + } + } + } +} + +func (s *BackrestHandler) GetOperations(ctx context.Context, req *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) { + q, err := protoutil.OpSelectorToQuery(req.Msg.Selector) + if req.Msg.LastN != 0 { + q.Reversed = true + q.Limit = int(req.Msg.LastN) + } + if err != nil { + return nil, err + } + + var ops []*v1.Operation + opCollector := func(op *v1.Operation) error { + ops = append(ops, op) + return nil + } + err = s.oplog.Query(q, opCollector) + if err != nil { + return nil, fmt.Errorf("failed to get operations: %w", err) + } + + slices.SortFunc(ops, func(i, j *v1.Operation) int { + if i.Id < j.Id { + return -1 + } + return 1 + }) + + return connect.NewResponse(&v1.OperationList{ + Operations: ops, + }), nil +} + +func (s *BackrestHandler) IndexSnapshots(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) { + // Ensure the repo is valid before scheduling the task + repo, err := s.orchestrator.GetRepo(req.Msg.Value) + if err != nil { + return nil, fmt.Errorf("failed to get repo %q: %w", req.Msg.Value, err) + } + + // Schedule the indexing task + if err := s.orchestrator.ScheduleTask(tasks.NewOneoffIndexSnapshotsTask(repo, time.Now()), tasks.TaskPriorityInteractive+tasks.TaskPriorityIndexSnapshots); err != nil { + return nil, fmt.Errorf("failed to schedule indexing task: %w", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +func (s *BackrestHandler) Backup(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) { + plan, err := s.orchestrator.GetPlan(req.Msg.Value) + if err != nil { + return nil, err + } + repo, err := s.orchestrator.GetRepo(plan.Repo) + if err != nil { + return nil, err + } + wait := make(chan struct{}) + if err := s.orchestrator.ScheduleTask(tasks.NewOneoffBackupTask(repo, plan, time.Now()), tasks.TaskPriorityInteractive, func(e error) { + err = e + close(wait) + }); err != nil { + return nil, err + } + <-wait + return connect.NewResponse(&emptypb.Empty{}), err +} + +func (s *BackrestHandler) Forget(ctx context.Context, req *connect.Request[v1.ForgetRequest]) (*connect.Response[emptypb.Empty], error) { + at := time.Now() + var err error + + repo, err := s.orchestrator.GetRepo(req.Msg.RepoId) + if err != nil { + return nil, err + } + + if req.Msg.SnapshotId != "" && req.Msg.PlanId != "" && req.Msg.RepoId != "" { + wait := make(chan struct{}) + if err := s.orchestrator.ScheduleTask( + tasks.NewOneoffForgetSnapshotTask(repo, req.Msg.PlanId, 0, at, req.Msg.SnapshotId), + tasks.TaskPriorityInteractive+tasks.TaskPriorityForget, func(e error) { + err = e + close(wait) + }); err != nil { + return nil, err + } + <-wait + } else if req.Msg.RepoId != "" && req.Msg.PlanId != "" { + wait := make(chan struct{}) + if err := s.orchestrator.ScheduleTask( + tasks.NewOneoffForgetTask(repo, req.Msg.PlanId, 0, at), + tasks.TaskPriorityInteractive+tasks.TaskPriorityForget, func(e error) { + err = e + close(wait) + }); err != nil { + return nil, err + } + <-wait + } else { + return nil, errors.New("must specify repoId and planId and (optionally) snapshotId") + } + if err != nil { + return nil, err + } + return connect.NewResponse(&emptypb.Empty{}), nil +} + +func (s BackrestHandler) DoRepoTask(ctx context.Context, req *connect.Request[v1.DoRepoTaskRequest]) (*connect.Response[emptypb.Empty], error) { + var task tasks.Task + + repo, err := s.orchestrator.GetRepo(req.Msg.RepoId) + if err != nil { + return nil, err + } + + priority := tasks.TaskPriorityInteractive + switch req.Msg.Task { + case v1.DoRepoTaskRequest_TASK_CHECK: + task = tasks.NewCheckTask(repo, tasks.PlanForSystemTasks, true) + case v1.DoRepoTaskRequest_TASK_PRUNE: + task = tasks.NewPruneTask(repo, tasks.PlanForSystemTasks, true) + priority |= tasks.TaskPriorityPrune + case v1.DoRepoTaskRequest_TASK_STATS: + task = tasks.NewStatsTask(repo, tasks.PlanForSystemTasks, true) + priority |= tasks.TaskPriorityStats + case v1.DoRepoTaskRequest_TASK_INDEX_SNAPSHOTS: + task = tasks.NewOneoffIndexSnapshotsTask(repo, time.Now()) + priority |= tasks.TaskPriorityIndexSnapshots + case v1.DoRepoTaskRequest_TASK_UNLOCK: + repo, err := s.orchestrator.GetRepoOrchestrator(req.Msg.RepoId) + if err != nil { + return nil, err + } + if err := repo.Unlock(ctx); err != nil { + return nil, fmt.Errorf("failed to unlock repo %q: %w", req.Msg.RepoId, err) + } + return connect.NewResponse(&emptypb.Empty{}), nil + default: + return nil, fmt.Errorf("unknown task %v", req.Msg.Task.String()) + } + + wait := make(chan struct{}) + if err := s.orchestrator.ScheduleTask(task, priority, func(e error) { + err = e + close(wait) + }); err != nil { + return nil, err + } + <-wait + if err != nil { + return nil, err + } + return connect.NewResponse(&emptypb.Empty{}), nil +} + +func (s *BackrestHandler) Restore(ctx context.Context, req *connect.Request[v1.RestoreSnapshotRequest]) (*connect.Response[emptypb.Empty], error) { + req.Msg.Target = strings.TrimSpace(req.Msg.Target) + req.Msg.Path = strings.TrimSpace(req.Msg.Path) + + if req.Msg.Target == "" { + req.Msg.Target = path.Join(os.Getenv("HOME"), "Downloads", fmt.Sprintf("restic-restore-%v", time.Now().Format("2006-01-02T15-04-05"))) + } + if req.Msg.Path == "" { + req.Msg.Path = "/" + } + // prevent restoring to a directory that already exists + if _, err := os.Stat(req.Msg.Target); err == nil { + return nil, fmt.Errorf("target directory %q already exists", req.Msg.Target) + } + + repo, err := s.orchestrator.GetRepo(req.Msg.RepoId) + if err != nil { + return nil, err + } + + at := time.Now() + s.orchestrator.ScheduleTask(tasks.NewOneoffRestoreTask(repo, req.Msg.PlanId, 0 /* flowID */, at, req.Msg.SnapshotId, req.Msg.Path, req.Msg.Target), tasks.TaskPriorityInteractive+tasks.TaskPriorityDefault) + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +func (s *BackrestHandler) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + cfg, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + repo := config.FindRepo(cfg, req.Msg.RepoId) + if repo == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("repo %q not found", req.Msg.RepoId)) + } + + // group commands within the last 24 hours (or 256 operations) into the same flow ID + var flowID int64 + if s.oplog.Query(oplog.Query{}. + SetInstanceID(cfg.Instance). + SetRepoGUID(repo.GetGuid()). + SetLimit(256). + SetReversed(true), func(op *v1.Operation) error { + if op.GetOperationRunCommand() != nil && time.Since(time.UnixMilli(op.UnixTimeStartMs)) < 30*time.Minute { + flowID = op.FlowId + } + return nil + }) != nil { + return nil, fmt.Errorf("failed to query operations") + } + + task := tasks.NewOneoffRunCommandTask(repo, tasks.PlanForSystemTasks, flowID, time.Now(), req.Msg.Command) + st, err := s.orchestrator.CreateUnscheduledTask(task, tasks.TaskPriorityInteractive, time.Now()) + if err != nil { + return nil, fmt.Errorf("failed to create task: %w", err) + } + if err := s.orchestrator.RunTask(context.Background(), st); err != nil { + return nil, fmt.Errorf("failed to run command: %w", err) + } + + return connect.NewResponse(&types.Int64Value{Value: st.Op.GetId()}), nil +} + +func (s *BackrestHandler) Cancel(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) { + if err := s.orchestrator.CancelOperation(req.Msg.Value, v1.OperationStatus_STATUS_USER_CANCELLED); err != nil { + return nil, err + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) { + var err error + var ids []int64 + + opCollector := func(op *v1.Operation) error { + if !req.Msg.OnlyFailed || op.Status == v1.OperationStatus_STATUS_ERROR { + ids = append(ids, op.Id) + } + return nil + } + + q, err := protoutil.OpSelectorToQuery(req.Msg.Selector) + if err != nil { + return nil, err + } + if err := s.oplog.Query(q, opCollector); err != nil { + return nil, fmt.Errorf("failed to get operations to delete: %w", err) + } + + if err := s.oplog.Delete(ids...); err != nil { + return nil, fmt.Errorf("failed to delete operations: %w", err) + } + + return connect.NewResponse(&emptypb.Empty{}), err +} + +func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest], resp *connect.ServerStream[types.BytesValue]) error { + r, err := s.logStore.Open(req.Msg.Ref) + if err != nil { + if errors.Is(err, logstore.ErrLogNotFound) { + resp.Send(&types.BytesValue{ + Value: []byte(fmt.Sprintf("file associated with log %v not found, it may have expired.", req.Msg.GetRef())), + }) + return nil + } + return fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) + } + go func() { + <-ctx.Done() + r.Close() + }() + + var errChan = make(chan error, 1) + var sendChan = make(chan []byte, 1) + var buffer bytes.Buffer + var bufferMu sync.Mutex + + go func() { + data := make([]byte, 4*1024) + for { + n, err := r.Read(data) + if n == 0 { + break + } else if err != nil && err != io.EOF { + errChan <- fmt.Errorf("failed to read log data: %w", err) + close(errChan) + break + } + bufferMu.Lock() + buffer.Write(data[:n]) + if buffer.Len() > 128*1024 { + sendChan <- bytes.Clone(buffer.Bytes()) + buffer.Reset() + } + bufferMu.Unlock() + } + + if buffer.Len() > 0 { + bufferMu.Lock() + sendChan <- bytes.Clone(buffer.Bytes()) + buffer.Reset() + bufferMu.Unlock() + } + close(sendChan) + }() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case data, ok := <-sendChan: + if !ok { + return nil + } + if err := resp.Send(&types.BytesValue{Value: data}); err != nil { + bufferMu.Lock() + buffer.Write(data) + bufferMu.Unlock() + return err + } + case err := <-errChan: + return err + case <-ticker.C: + bufferMu.Lock() + if buffer.Len() > 0 { + if err := resp.Send(&types.BytesValue{Value: bytes.Clone(buffer.Bytes())}); err != nil { + bufferMu.Unlock() + return err + } + buffer.Reset() + } + bufferMu.Unlock() + } + } + +} + +func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { + op, err := s.oplog.Get(req.Msg.Value) + if err != nil { + return nil, fmt.Errorf("failed to get operation %v: %w", req.Msg.Value, err) + } + _, ok := op.Op.(*v1.Operation_OperationRestore) + if !ok { + return nil, fmt.Errorf("operation %v is not a restore operation", req.Msg.Value) + } + signature, err := signInt64(op.Id) // the signature authenticates the download URL. Note that the shared URL will be valid for any downloader. + if err != nil { + return nil, fmt.Errorf("failed to generate signature: %w", err) + } + return connect.NewResponse(&types.StringValue{ + Value: fmt.Sprintf("./download/%x-%s/", op.Id, hex.EncodeToString(signature)), + }), nil +} + +func (s *BackrestHandler) PathAutocomplete(ctx context.Context, path *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) { + ents, err := os.ReadDir(path.Msg.Value) + if errors.Is(err, os.ErrNotExist) { + return connect.NewResponse(&types.StringList{}), nil + } else if err != nil { + return nil, err + } + + var paths []string + for _, ent := range ents { + paths = append(paths, ent.Name()) + } + + return connect.NewResponse(&types.StringList{Values: paths}), nil +} + +func (s *BackrestHandler) GetSummaryDashboard(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + config, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + generateSummaryHelper := func(id string, q oplog.Query) (*v1.SummaryDashboardResponse_Summary, error) { + var backupsExamined int64 + var bytesScanned30 int64 + var bytesAdded30 int64 + var backupsFailed30 int64 + var backupsSuccess30 int64 + var backupsWarning30 int64 + var nextBackupTime int64 + backupChart := &v1.SummaryDashboardResponse_BackupChart{} + + s.oplog.Query(q, func(op *v1.Operation) error { + t := time.UnixMilli(op.UnixTimeStartMs) + + if backupOp := op.GetOperationBackup(); backupOp != nil { + if time.Since(t) > 30*24*time.Hour { + return oplog.ErrStopIteration + } else if op.GetStatus() == v1.OperationStatus_STATUS_PENDING { + nextBackupTime = op.UnixTimeStartMs + return nil + } + backupsExamined++ + + if op.Status == v1.OperationStatus_STATUS_SUCCESS { + backupsSuccess30++ + } else if op.Status == v1.OperationStatus_STATUS_ERROR { + backupsFailed30++ + } else if op.Status == v1.OperationStatus_STATUS_WARNING { + backupsWarning30++ + } + + if summary := backupOp.GetLastStatus().GetSummary(); summary != nil { + bytesScanned30 += summary.TotalBytesProcessed + bytesAdded30 += summary.DataAdded + } + + // recent backups chart + if len(backupChart.TimestampMs) < 60 { // only include the latest 90 backups in the chart + duration := op.UnixTimeEndMs - op.UnixTimeStartMs + if duration <= 1000 { + duration = 1000 + } + + backupChart.FlowId = append(backupChart.FlowId, op.FlowId) + backupChart.TimestampMs = append(backupChart.TimestampMs, op.UnixTimeStartMs) + backupChart.DurationMs = append(backupChart.DurationMs, duration) + backupChart.Status = append(backupChart.Status, op.Status) + backupChart.BytesAdded = append(backupChart.BytesAdded, backupOp.GetLastStatus().GetSummary().GetDataAdded()) + } + } + + return nil + }) + + if backupsExamined == 0 { + backupsExamined = 1 // prevent division by zero for avg calculations + } + + return &v1.SummaryDashboardResponse_Summary{ + Id: id, + BytesScannedLast_30Days: bytesScanned30, + BytesAddedLast_30Days: bytesAdded30, + BackupsFailed_30Days: backupsFailed30, + BackupsWarningLast_30Days: backupsWarning30, + BackupsSuccessLast_30Days: backupsSuccess30, + BytesScannedAvg: bytesScanned30 / backupsExamined, + BytesAddedAvg: bytesAdded30 / backupsExamined, + NextBackupTimeMs: nextBackupTime, + RecentBackups: backupChart, + }, nil + } + + response := &v1.SummaryDashboardResponse{ + ConfigPath: env.ConfigFilePath(), + DataPath: env.DataDir(), + } + + for _, repo := range config.Repos { + resp, err := generateSummaryHelper(repo.Id, oplog.Query{}. + SetInstanceID(config.Instance). + SetRepoGUID(repo.GetGuid()). + SetReversed(true). + SetLimit(1000)) + if err != nil { + return nil, fmt.Errorf("summary for repo %q: %w", repo.Id, err) + } + + response.RepoSummaries = append(response.RepoSummaries, resp) + } + + for _, plan := range config.Plans { + resp, err := generateSummaryHelper(plan.Id, oplog.Query{}. + SetInstanceID(config.Instance). + SetPlanID(plan.Id). + SetReversed(true). + SetLimit(1000)) + if err != nil { + return nil, fmt.Errorf("summary for plan %q: %w", plan.Id, err) + } + + response.PlanSummaries = append(response.PlanSummaries, resp) + } + + return connect.NewResponse(response), nil +} diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go new file mode 100644 index 000000000..404eec3bd --- /dev/null +++ b/internal/api/backresthandler_test.go @@ -0,0 +1,1111 @@ +package api + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + syncapi "github.com/garethgeorge/backrest/internal/api/syncapi" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" + "github.com/garethgeorge/backrest/internal/orchestrator" + "github.com/garethgeorge/backrest/internal/resticinstaller" + "github.com/garethgeorge/backrest/internal/testutil" + "github.com/hashicorp/go-multierror" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" +) + +func createConfigManager(cfg *v1.Config) *config.ConfigManager { + return &config.ConfigManager{ + Store: &config.MemoryStore{ + Config: cfg, + }, + } +} + +func TestUpdateConfig(t *testing.T) { + t.Parallel() + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + })) + + tests := []struct { + name string + req *v1.Config + wantErr bool + res *v1.Config + }{ + { + name: "bad modno", + req: &v1.Config{ + Modno: 4321, + }, + wantErr: true, + }, + { + name: "good modno", + req: &v1.Config{ + Modno: 1234, + Instance: "test", + }, + wantErr: false, + res: &v1.Config{ + Modno: 1235, + Instance: "test", + }, + }, + { + name: "reject when validation fails", + req: &v1.Config{ + Modno: 1235, + Instance: "test", + Repos: []*v1.Repo{ + {}, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + res, err := sut.handler.SetConfig(ctx, connect.NewRequest(tt.req)) + if (err != nil) != tt.wantErr { + t.Errorf("SetConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err == nil && !proto.Equal(res.Msg, tt.res) { + t.Errorf("SetConfig() got = %v, want %v", res, tt.res) + } + }) + } +} + +func TestRemoveRepo(t *testing.T) { + t.Parallel() + + mgr := createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + }, + }, + }) + sut := createSystemUnderTest(t, mgr) + + // insert an operation that should get removed + if _, err := sut.handler.RunCommand(context.Background(), connect.NewRequest(&v1.RunCommandRequest{ + RepoId: "local", + Command: "help", + })); err != nil { + t.Fatalf("RunCommand() error = %v", err) + } + + // assert that the operation exists + ops := getOperations(t, sut.oplog) + if len(ops) != 1 { + t.Fatalf("expected 1 operation, got %d", len(ops)) + } + + if _, err := sut.handler.RemoveRepo(context.Background(), connect.NewRequest(&types.StringValue{ + Value: "local", + })); err != nil { + t.Fatalf("RemoveRepo() error = %v", err) + } + + // assert that the operation has been removed + ops = getOperations(t, sut.oplog) + if len(ops) != 0 { + t.Fatalf("expected 0 operations, got %d", len(ops)) + } +} + +func TestBackup(t *testing.T) { + t.Parallel() + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Retention: &v1.RetentionPolicy{ + Policy: &v1.RetentionPolicy_PolicyKeepLastN{PolicyKeepLastN: 100}, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + _, err := sut.handler.Backup(ctx, connect.NewRequest(&types.StringValue{Value: "test"})) + if err != nil { + t.Fatalf("Backup() error = %v", err) + } + + // Wait for the backup to complete. + if err := testutil.Retry(t, ctx, func() error { + ops := getOperations(t, sut.oplog) + if slices.IndexFunc(ops, func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }) == -1 { + return fmt.Errorf("expected a backup operation, got %v", ops) + } + return nil + }); err != nil { + t.Fatalf("Couldn't find backup operation in oplog") + } + + // Wait for the index snapshot operation to appear in the oplog. + var snapshotOp *v1.Operation + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationIndexSnapshot) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }); index != -1 { + snapshotOp = operations[index] + return nil + } + return errors.New("snapshot not indexed") + }); err != nil { + t.Fatalf("Couldn't find snapshot in oplog") + } + + if snapshotOp.SnapshotId == "" { + t.Fatalf("snapshotId must be set") + } + + // Wait for a forget operation to appear in the oplog. + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationForget) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }); index != -1 { + op := operations[index] + if op.FlowId != snapshotOp.FlowId { + t.Fatalf("Flow ID mismatch on forget operation") + } + return nil + } + return errors.New("forget not indexed") + }); err != nil { + t.Fatalf("Couldn't find forget in oplog") + } +} + +func TestMultipleBackup(t *testing.T) { + t.Parallel() + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Retention: &v1.RetentionPolicy{ + Policy: &v1.RetentionPolicy_PolicyKeepLastN{ + PolicyKeepLastN: 1, + }, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + + go func() { + sut.orch.Run(ctx) + }() + + for i := 0; i < 2; i++ { + _, err := sut.handler.Backup(ctx, connect.NewRequest(&types.StringValue{Value: "test"})) + if err != nil { + t.Fatalf("Backup() error = %v", err) + } + } + + // Wait for a forget that removed 1 snapshot to appear in the oplog + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { + forget, ok := op.GetOp().(*v1.Operation_OperationForget) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok && len(forget.OperationForget.Forget) > 0 + }); index == -1 { + return errors.New("forget not indexed") + } else if len(operations[index].GetOp().(*v1.Operation_OperationForget).OperationForget.Forget) != 1 { + return fmt.Errorf("expected 1 item removed in the forget operation, got %d", len(operations[index].GetOp().(*v1.Operation_OperationForget).OperationForget.Forget)) + } + return nil + }); err != nil { + t.Fatalf("Couldn't find forget with 1 item removed in the operation log") + } +} + +func TestHookExecution(t *testing.T) { + t.Parallel() + dir := t.TempDir() + hookOutputBefore := path.Join(dir, "before.txt") + hookOutputAfter := path.Join(dir, "after.txt") + + commandBefore := fmt.Sprintf("echo before > %s", hookOutputBefore) + commandAfter := fmt.Sprintf("echo after > %s", hookOutputAfter) + if runtime.GOOS == "windows" { + commandBefore = fmt.Sprintf("echo \"before\" | Out-File -FilePath %q", hookOutputBefore) + commandAfter = fmt.Sprintf("echo \"after\" | Out-File -FilePath %q", hookOutputAfter) + } + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: commandBefore, + }, + }, + }, + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_END, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: commandAfter, + }, + }, + }, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + _, err := sut.handler.Backup(ctx, connect.NewRequest(&types.StringValue{Value: "test"})) + if err != nil { + t.Fatalf("Backup() error = %v", err) + } + + // Wait for two hook operations to appear in the oplog + if err := testutil.Retry(t, ctx, func() error { + hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationRunHook) + return !ok + }) + + if len(hookOps) == 2 { + return nil + } + return fmt.Errorf("expected 2 hook operations, got %d", len(hookOps)) + }); err != nil { + t.Fatalf("Couldn't find hooks in oplog: %v", err) + } + + // expect the hook output files to exist + if _, err := os.Stat(hookOutputBefore); err != nil { + t.Fatalf("hook output file before not found") + } + + if _, err := os.Stat(hookOutputAfter); err != nil { + t.Fatalf("hook output file after not found") + } +} + +func TestHookOnErrorHandling(t *testing.T) { + t.Parallel() + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test-cancel", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_CANCEL, + }, + }, + }, + { + Id: "test-error", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_FATAL, + }, + }, + }, + { + Id: "test-ignore", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_IGNORE, + }, + }, + }, + { + Id: "test-retry", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_RETRY_10MINUTES, + }, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + tests := []struct { + name string + plan string + wantHookStatus v1.OperationStatus + wantBackupStatus v1.OperationStatus + wantBackupError bool + noWaitForBackup bool + }{ + { + name: "cancel", + plan: "test-cancel", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_USER_CANCELLED, + wantBackupError: true, + }, + { + name: "error", + plan: "test-error", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupError: true, + }, + { + name: "ignore", + plan: "test-ignore", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_SUCCESS, + wantBackupError: false, + }, + { + name: "retry", + plan: "test-retry", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_PENDING, + wantBackupError: false, + noWaitForBackup: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + sut.opstore.ResetForTest(t) + + var errgroup errgroup.Group + + errgroup.Go(func() error { + _, err := sut.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: tc.plan})) + if (err != nil) != tc.wantBackupError { + return fmt.Errorf("Backup() error = %v, wantErr %v", err, tc.wantBackupError) + } + return nil + }) + + if !tc.noWaitForBackup { + if err := errgroup.Wait(); err != nil { + t.Fatalf("%s", err.Error()) + } + } + + // Wait for hook operation to be attempted in the oplog + if err := testutil.Retry(t, ctx, func() error { + hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationRunHook) + return !ok + }) + if len(hookOps) != 1 { + return fmt.Errorf("expected 1 hook operations, got %d", len(hookOps)) + } + if hookOps[0].Status != tc.wantHookStatus { + return fmt.Errorf("expected hook operation error status, got %v", hookOps[0].Status) + } + return nil + }); err != nil { + t.Fatalf("Couldn't find hook operation in oplog: %v", err) + } + + if err := testutil.Retry(t, ctx, func() error { + backupOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return !ok + }) + if len(backupOps) != 1 { + return fmt.Errorf("expected 1 backup operation, got %d", len(backupOps)) + } + if backupOps[0].Status != tc.wantBackupStatus { + return fmt.Errorf("expected backup operation status %v, got %v", tc.wantBackupStatus, backupOps[0].Status) + } + return nil + }); err != nil { + t.Fatalf("Failed to verify backup operation: %v", err) + } + }) + } +} + +func TestCancelBackup(t *testing.T) { + t.Parallel() + + // a hook is used to make the backup operation wait long enough to be cancelled + hookCmd := "sleep 2" + if runtime.GOOS == "windows" { + hookCmd = "Start-Sleep -Seconds 2" + } + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Retention: &v1.RetentionPolicy{ + Policy: &v1.RetentionPolicy_PolicyKeepLastN{ + PolicyKeepLastN: 1, + }, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: hookCmd, + }, + }, + }, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + go func() { + backupReq := &types.StringValue{Value: "test"} + _, err := sut.handler.Backup(ctx, connect.NewRequest(backupReq)) + if err != nil { + t.Logf("Backup() error = %v", err) + } + }() + + // Find the in-progress backup operation ID in the oplog, waits for the task to be in progress before attempting to cancel. + var backupOpId int64 + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + for _, op := range operations { + if op.GetOperationBackup() != nil && op.Status == v1.OperationStatus_STATUS_INPROGRESS { + backupOpId = op.Id + return nil + } + } + return errors.New("backup operation not found") + }); err != nil { + t.Fatalf("Couldn't find backup operation in oplog") + } + + if _, err := sut.handler.Cancel(context.Background(), connect.NewRequest(&types.Int64Value{Value: backupOpId})); err != nil { + t.Errorf("Cancel() error = %v, wantErr nil", err) + } + + if err := testutil.Retry(t, ctx, func() error { + if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return op.Status == v1.OperationStatus_STATUS_ERROR && ok + }) == -1 { + return errors.New("backup operation not found") + } + return nil + }); err != nil { + t.Fatalf("Couldn't find failed canceled backup operation in oplog") + } +} + +func TestRestore(t *testing.T) { + t.Parallel() + + backupDataDir := t.TempDir() + if err := os.WriteFile(filepath.Join(backupDataDir, "findme.txt"), []byte("test data"), 0644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + Plans: []*v1.Plan{ + { + Id: "test", + Repo: "local", + Paths: []string{ + backupDataDir, + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Retention: &v1.RetentionPolicy{ + Policy: &v1.RetentionPolicy_PolicyKeepAll{PolicyKeepAll: true}, + }, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + _, err := sut.handler.Backup(ctx, connect.NewRequest(&types.StringValue{Value: "test"})) + if err != nil { + t.Fatalf("Backup() error = %v", err) + } + + // Wait for the backup to complete. + if err := testutil.Retry(t, ctx, func() error { + // Check that there is a successful backup recorded in the log. + if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }) == -1 { + return errors.New("Expected a backup operation") + } + return nil + }); err != nil { + t.Fatalf("Couldn't find backup operation in oplog") + } + + // Wait for the index snapshot operation to appear in the oplog. + var snapshotOp *v1.Operation + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationIndexSnapshot) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }); index != -1 { + snapshotOp = operations[index] + return nil + } + return errors.New("snapshot not indexed") + }); err != nil { + t.Fatalf("Couldn't find snapshot in oplog") + } + + if snapshotOp.SnapshotId == "" { + t.Fatalf("snapshotId must be set") + } + + restoreTarget := t.TempDir() + "/restore" + + _, err = sut.handler.Restore(ctx, connect.NewRequest(&v1.RestoreSnapshotRequest{ + SnapshotId: snapshotOp.SnapshotId, + PlanId: "test", + RepoId: "local", + Target: restoreTarget, + })) + if err != nil { + t.Fatalf("Restore() error = %v", err) + } + + // Wait for a restore operation to appear in the oplog. + if err := testutil.Retry(t, ctx, func() error { + operations := getOperations(t, sut.oplog) + if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationRestore) + return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok + }); index != -1 { + op := operations[index] + if op.SnapshotId != snapshotOp.SnapshotId { + t.Errorf("Snapshot ID mismatch on restore operation") + } + return nil + } + return errors.New("restore not indexed") + }); err != nil { + t.Fatalf("Couldn't find restore in oplog") + } + + // Check that the restore target contains the expected file. + var files []string + filepath.Walk(restoreTarget, func(path string, info fs.FileInfo, err error) error { + if err != nil { + t.Errorf("Walk() error = %v", err) + } + files = append(files, path) + return nil + }) + t.Logf("files: %v", files) + if !slices.ContainsFunc(files, func(s string) bool { + return strings.HasSuffix(s, "findme.txt") + }) { + t.Fatalf("Expected file not found in restore target") + } +} + +func TestRunCommand(t *testing.T) { + sut := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + })) + + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + go func() { + sut.orch.Run(ctx) + }() + + res, err := sut.handler.RunCommand(ctx, connect.NewRequest(&v1.RunCommandRequest{ + RepoId: "local", + Command: "help", + })) + if err != nil { + t.Fatalf("RunCommand() error = %v", err) + } + op, err := sut.oplog.Get(res.Msg.Value) + if err != nil { + t.Fatalf("Failed to find runcommand operation: %v", err) + } + + if op.Status != v1.OperationStatus_STATUS_SUCCESS { + t.Fatalf("Expected runcommand operation to succeed") + } + + cmdOp := op.GetOperationRunCommand() + if cmdOp == nil { + t.Fatalf("Expected runcommand operation to be of type OperationRunCommand") + } + if cmdOp.Command != "help" { + t.Fatalf("Expected runcommand operation to have correct command") + } + if cmdOp.OutputLogref == "" { + t.Fatalf("Expected runcommand operation to have output logref") + } + + log, err := sut.logStore.Open(cmdOp.OutputLogref) + if err != nil { + t.Fatalf("Failed to open log: %v", err) + } + defer log.Close() + + data, err := io.ReadAll(log) + if err != nil { + t.Fatalf("Failed to read log: %v", err) + } + if !strings.Contains(string(data), "Usage") { + t.Fatalf("Expected log output to contain help text") + } +} + +func TestMultihostIndexSnapshots(t *testing.T) { + t.Parallel() + ctx, cancel := testutil.WithDeadlineFromTest(t, context.Background()) + defer cancel() + + emptyDir := t.TempDir() + repoGUID := cryptoutil.MustRandomID(cryptoutil.DefaultIDBits) + + repo1 := &v1.Repo{ + Id: "local1", + Guid: repoGUID, + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + } + repo2 := proto.Clone(repo1).(*v1.Repo) + repo2.Id = "local2" + + plan1 := &v1.Plan{ + Id: "test1", + Repo: "local1", + Paths: []string{ + emptyDir, + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + } + plan2 := proto.Clone(plan1).(*v1.Plan) + plan2.Id = "test2" + plan2.Repo = "local2" + + host1 := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test1", + Repos: []*v1.Repo{ + repo1, + }, + Plans: []*v1.Plan{ + plan1, + }, + })) + go func() { + host1.orch.Run(ctx) + }() + + host2 := createSystemUnderTest(t, createConfigManager(&v1.Config{ + Modno: 1234, + Instance: "test2", + Repos: []*v1.Repo{ + repo2, + }, + Plans: []*v1.Plan{ + plan2, + }, + })) + go func() { + host2.orch.Run(ctx) + }() + + host1.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: "test1"})) + host2.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: "test2"})) + + for i := 0; i < 1; i++ { + if _, err := host1.handler.IndexSnapshots(context.Background(), connect.NewRequest(&types.StringValue{Value: "local1"})); err != nil { + t.Errorf("local1 sut1 IndexSnapshots() error = %v", err) + } + if _, err := host2.handler.IndexSnapshots(context.Background(), connect.NewRequest(&types.StringValue{Value: "local2"})); err != nil { + t.Errorf("local2 sut2 IndexSnapshots() error = %v", err) + } + } + + findSnapshotsFromInstance := func(ops []*v1.Operation, inst string) []*v1.Operation { + output := []*v1.Operation{} + for _, op := range ops { + if op.GetOperationIndexSnapshot() != nil && op.InstanceId == inst { + output = append(output, op) + } + } + return output + } + + countSnapshotOperations := func(ops []*v1.Operation) int { + count := 0 + for _, op := range ops { + if op.GetOperationIndexSnapshot() != nil { + count++ + } + } + return count + } + + var ops []*v1.Operation + var ops2 []*v1.Operation + testutil.TryNonfatal(t, ctx, func() error { + ops = getOperations(t, host1.oplog) + ops2 = getOperations(t, host2.oplog) + var err error + for _, logOps := range []struct { + ops []*v1.Operation + instance string + }{ + {ops, "test1"}, + {ops, "test2"}, + {ops2, "test1"}, + {ops2, "test2"}, + } { + snapshotOps := findSnapshotsFromInstance(logOps.ops, logOps.instance) + if len(snapshotOps) != 1 { + err = multierror.Append(err, fmt.Errorf("expected exactly 1 snapshot from %s, got %d", logOps.instance, len(snapshotOps))) + } + } + return err + }) + + if countSnapshotOperations(ops) != 2 { + t.Errorf("expected exactly 2 snapshot operation in sut1 log, got %d", countSnapshotOperations(ops)) + } + if countSnapshotOperations(ops2) != 2 { + t.Errorf("expected exactly 2 snapshot operation in sut2 log, got %d", countSnapshotOperations(ops2)) + } +} + +type systemUnderTest struct { + handler *BackrestHandler + oplog *oplog.OpLog + opstore *sqlitestore.SqliteStore + orch *orchestrator.Orchestrator + logStore *logstore.LogStore + config *v1.Config +} + +func createSystemUnderTest(t *testing.T, config *config.ConfigManager) systemUnderTest { + dir := t.TempDir() + + cfg, err := config.Get() + if err != nil { + t.Fatalf("Failed to get config: %v", err) + } + remoteConfigStore := syncapi.NewJSONDirRemoteConfigStore(dir) + resticBin, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + t.Fatalf("Failed to find or install restic binary: %v", err) + } + opstore, err := sqlitestore.NewSqliteStore(filepath.Join(dir, "oplog.sqlite")) + if err != nil { + t.Fatalf("Failed to create opstore: %v", err) + } + t.Cleanup(func() { opstore.Close() }) + oplog, err := oplog.NewOpLog(opstore) + if err != nil { + t.Fatalf("Failed to create oplog: %v", err) + } + logStore, err := logstore.NewLogStore(filepath.Join(dir, "tasklogs")) + if err != nil { + t.Fatalf("Failed to create log store: %v", err) + } + t.Cleanup(func() { logStore.Close() }) + orch, err := orchestrator.NewOrchestrator( + resticBin, config, oplog, logStore, + ) + if err != nil { + t.Fatalf("Failed to create orchestrator: %v", err) + } + + for _, repo := range cfg.Repos { + rorch, err := orch.GetRepoOrchestrator(repo.Id) + if err != nil { + t.Fatalf("Failed to get repo %s: %v", repo.Id, err) + } + + if err := rorch.Init(context.Background()); err != nil { + t.Fatalf("Failed to init repo %s: %v", repo.Id, err) + } + } + + h := NewBackrestHandler(config, remoteConfigStore, orch, oplog, logStore) + + return systemUnderTest{ + handler: h, + oplog: oplog, + opstore: opstore, + orch: orch, + logStore: logStore, + config: cfg, + } +} + +func getOperations(t *testing.T, log *oplog.OpLog) []*v1.Operation { + operations := []*v1.Operation{} + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { + operations = append(operations, op) + return nil + }); err != nil { + t.Fatalf("Failed to read oplog: %v", err) + } + return operations +} diff --git a/internal/api/downloadhandler.go b/internal/api/downloadhandler.go new file mode 100644 index 000000000..af5989edb --- /dev/null +++ b/internal/api/downloadhandler.go @@ -0,0 +1,144 @@ +package api + +import ( + "archive/tar" + "compress/gzip" + "crypto/hmac" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "go.uber.org/zap" +) + +func NewDownloadHandler(oplog *oplog.OpLog) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path[1:] + + opID, signature, filePath, err := parseDownloadPath(p) + if err != nil { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + + if ok, err := checkDownloadURLSignature(opID, signature); err != nil || !ok { + http.Error(w, fmt.Sprintf("invalid signature: %v", err), http.StatusForbidden) + return + } + + op, err := oplog.Get(int64(opID)) + if err != nil { + http.Error(w, "restore not found", http.StatusNotFound) + return + } + restoreOp, ok := op.Op.(*v1.Operation_OperationRestore) + if !ok { + http.Error(w, "restore not found", http.StatusNotFound) + return + } + targetPath := restoreOp.OperationRestore.GetTarget() + if targetPath == "" { + http.Error(w, "restore target not found", http.StatusNotFound) + return + } + fullPath := filepath.Join(targetPath, filePath) + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=archive-%v.tar.gz", time.Now().Format("2006-01-02-15-04-05"))) + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Content-Transfer-Encoding", "binary") + + gzw, err := gzip.NewWriterLevel(w, gzip.BestSpeed) + if err != nil { + zap.S().Errorf("error creating gzip writer: %v", err) + http.Error(w, "error creating gzip writer", http.StatusInternalServerError) + return + } + if err := tarDirectory(gzw, fullPath); err != nil { + zap.S().Errorf("error creating tar archive: %v", err) + http.Error(w, "error creating tar archive", http.StatusInternalServerError) + return + } + if err := gzw.Close(); err != nil { + http.Error(w, "error creating tar archive", http.StatusInternalServerError) + } + }) +} + +func parseDownloadPath(p string) (int64, string, string, error) { + sep := strings.Index(p, "/") + if sep == -1 { + return 0, "", "", fmt.Errorf("invalid path") + } + restoreID := p[:sep] + filePath := p[sep+1:] + + dash := strings.Index(restoreID, "-") + if dash == -1 { + return 0, "", "", fmt.Errorf("invalid restore ID") + } + opID, err := strconv.ParseInt(restoreID[:dash], 16, 64) + if err != nil { + return 0, "", "", fmt.Errorf("invalid restore ID: %w", err) + } + signature := restoreID[dash+1:] + return opID, signature, filePath, nil +} + +func checkDownloadURLSignature(id int64, signature string) (bool, error) { + wantSignatureBytes, err := signInt64(id) + if err != nil { + return false, err + } + signatureBytes, err := hex.DecodeString(signature) + if err != nil { + return false, err + } + return hmac.Equal(wantSignatureBytes, signatureBytes), nil +} + +func tarDirectory(w io.Writer, dirpath string) error { + t := tar.NewWriter(w) + if err := filepath.Walk(dirpath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + stat, err := os.Stat(path) + if err != nil { + return fmt.Errorf("stat %v: %w", path, err) + } + file, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("open %v: %w", path, err) + } + defer file.Close() + + if err := t.WriteHeader(&tar.Header{ + Name: path[len(dirpath)+1:], + Size: stat.Size(), + Mode: int64(stat.Mode()), + ModTime: stat.ModTime(), + }); err != nil { + return err + } + if n, err := io.CopyN(t, file, stat.Size()); err != nil { + zap.L().Warn("error copying file to tar archive", zap.String("path", path), zap.Error(err)) + } else if n != stat.Size() { + zap.L().Warn("error copying file to tar archive: short write", zap.String("path", path)) + } + return nil + }); err != nil { + return err + } + return t.Flush() +} diff --git a/internal/api/server.go b/internal/api/server.go deleted file mode 100644 index 6312cfb77..000000000 --- a/internal/api/server.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -import ( - "context" - "fmt" - "time" - - v1 "github.com/garethgeorge/resticui/gen/go/v1" - "github.com/garethgeorge/resticui/internal/config" - "go.uber.org/zap" - "google.golang.org/protobuf/types/known/emptypb" -) - -type server struct { - *v1.UnimplementedResticUIServer -} - -var _ v1.ResticUIServer = &server{} - -func (s *server) GetConfig(ctx context.Context, empty *emptypb.Empty) (*v1.Config, error) { - return config.Default.Get() -} - -func (s *server) SetConfig(ctx context.Context, c *v1.Config) (*v1.Config, error) { - err := config.Default.Update(c) - if err != nil { - return nil, fmt.Errorf("failed to update config: %w", err) - } - return config.Default.Get() -} - -func (s *server) GetEvents(_ *emptypb.Empty, stream v1.ResticUI_GetEventsServer) error { - for { - zap.S().Info("Sending event") - stream.Send(&v1.Event{ - Timestamp: 0, - Event: &v1.Event_BackupStatusChange{ - BackupStatusChange: &v1.BackupStatusEvent{ - Status: v1.Status_IN_PROGRESS, - Percent: 0, - Plan: "myplan", - }, - }, - }) - - timer := time.NewTimer(time.Second * 1) - - select { - case <-stream.Context().Done(): - zap.S().Info("Get events hangup") - return nil - case <-timer.C: - } - } -} \ No newline at end of file diff --git a/internal/api/signing.go b/internal/api/signing.go new file mode 100644 index 000000000..975d6db55 --- /dev/null +++ b/internal/api/signing.go @@ -0,0 +1,33 @@ +package api + +import ( + "crypto" + "crypto/hmac" + "crypto/rand" + "encoding/binary" +) + +var ( + secret = make([]byte, 32) +) + +func init() { + n, err := rand.Read(secret) + if n != 32 || err != nil { + panic("failed to generate secret key; is /dev/urandom available?") + } +} + +func sign(data []byte) ([]byte, error) { + h := hmac.New(crypto.SHA256.New, secret) + if n, err := h.Write(data); n != len(data) || err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +func signInt64(data int64) ([]byte, error) { + dataBytes := make([]byte, 8) + binary.BigEndian.PutUint64(dataBytes, uint64(data)) + return sign(dataBytes) +} diff --git a/internal/api/syncapi/identity.go b/internal/api/syncapi/identity.go new file mode 100644 index 000000000..942703783 --- /dev/null +++ b/internal/api/syncapi/identity.go @@ -0,0 +1,123 @@ +package syncapi + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" +) + +var ( + curve = elliptic.P256() // ed25519 +) + +type Identity struct { + InstanceID string + credentialFile string + + privateKey *ecdsa.PrivateKey + publicKey *ecdsa.PublicKey +} + +func NewIdentity(instanceID, credentialFile string) (*Identity, error) { + i := &Identity{ + InstanceID: instanceID, + credentialFile: credentialFile, + } + if err := i.loadOrGenerateKey(); err != nil { + return nil, err + } + return i, nil +} + +func (i *Identity) loadOrGenerateKey() error { + privKeyBytes, errpriv := os.ReadFile(i.credentialFile) + pubKeyBytes, errpub := os.ReadFile(i.credentialFile + ".pub") + if errpriv != nil || errpub != nil { + if os.IsNotExist(errpriv) || os.IsNotExist(errpub) { + return i.generateKeys() + } + if errpriv != nil { + return fmt.Errorf("open private key: %w", errpriv) + } + if errpub != nil { + return fmt.Errorf("open public key: %w", errpub) + } + } + + privKeyBlock, _ := pem.Decode(privKeyBytes) + if privKeyBlock == nil { + return errors.New("no private key found in pem") + } + privKey, err := x509.ParseECPrivateKey(privKeyBlock.Bytes) + if err != nil { + return fmt.Errorf("parse private key: %w", err) + } + + pubKeyBlock, _ := pem.Decode(pubKeyBytes) + if pubKeyBlock == nil { + return errors.New("no public key found in pem") + } + pubKey, err := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes) + if err != nil { + return fmt.Errorf("parse public key: %w", err) + } + + i.privateKey = privKey + i.publicKey = pubKey.(*ecdsa.PublicKey) + + return nil +} + +func (i *Identity) generateKeys() error { + privKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return err + } + + i.privateKey = privKey + i.publicKey = &privKey.PublicKey + + privateKeyBytes, err := x509.MarshalECPrivateKey(i.privateKey) + if err != nil { + return fmt.Errorf("marshal private key: %w", err) + } + pemPrivateKeyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE", Bytes: privateKeyBytes}) + if err := os.WriteFile(i.credentialFile, pemPrivateKeyBytes, 0600); err != nil { + return fmt.Errorf("write private key: %w", err) + } + + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&i.privateKey.PublicKey) + if err != nil { + return fmt.Errorf("marshal public key: %w", err) + } + pemPublicKeyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PUBLIC", Bytes: publicKeyBytes}) + if err := os.WriteFile(i.credentialFile+".pub", pemPublicKeyBytes, 0600); err != nil { + return fmt.Errorf("write public key: %w", err) + } + + return nil +} + +func (i *Identity) SignMessage(message []byte) ([]byte, error) { + hash := sha256.Sum256(message) + + sig, err := ecdsa.SignASN1(rand.Reader, i.privateKey, hash[:]) + if err != nil { + return nil, err + } + return sig, nil +} + +func (i *Identity) VerifySignature(message, sig []byte) error { + hash := sha256.Sum256(message) + if !ecdsa.VerifyASN1(i.publicKey, hash[:], sig) { + return errors.New("signature verification failed") + } + return nil +} diff --git a/internal/api/syncapi/identity_test.go b/internal/api/syncapi/identity_test.go new file mode 100644 index 000000000..b66c3d92a --- /dev/null +++ b/internal/api/syncapi/identity_test.go @@ -0,0 +1,50 @@ +package syncapi + +import ( + "fmt" + "path/filepath" + "testing" +) + +func TestCreateAndLoad(t *testing.T) { + dir := t.TempDir() + + // Create a new identity + ident, err := NewIdentity("test-instance", filepath.Join(dir, "myidentity.pem")) + if err != nil { + t.Fatalf("failed to create identity: %v", err) + } + + // Load the identity + loaded, err := NewIdentity("test-instance", filepath.Join(dir, "myidentity.pem")) + if err != nil { + t.Fatalf("failed to load identity: %v", err) + } + + // Verify the identity + if !ident.privateKey.Equal(loaded.privateKey) { + t.Fatalf("identities do not match") + } +} + +func TestSignatures(t *testing.T) { + dir := t.TempDir() + + // Create a new identity + ident, err := NewIdentity("test-instance", filepath.Join(dir, "myidentity.pem")) + if err != nil { + t.Fatalf("failed to create identity: %v", err) + } + + // Sign a message + signature, err := ident.SignMessage([]byte("hello world!")) + if err != nil { + t.Fatalf("failed to sign message: %v", err) + } + fmt.Printf("signed message: %x\n", signature) + + // verify the signature + if err := ident.VerifySignature([]byte("hello world!"), signature); err != nil { + t.Fatalf("failed to verify signature: %v", err) + } +} diff --git a/internal/api/syncapi/remoteconfigstore.go b/internal/api/syncapi/remoteconfigstore.go new file mode 100644 index 000000000..49052ed9d --- /dev/null +++ b/internal/api/syncapi/remoteconfigstore.go @@ -0,0 +1,170 @@ +package syncapi + +import ( + "errors" + "fmt" + "hash/crc32" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +var ( + sanitizeFilenameRegex = regexp.MustCompile("[^a-zA-Z0-9\\-_\\.]+") + + ErrRemoteConfigNotFound = errors.New("remote config not found") +) + +type RemoteConfigStore interface { + // Get a remote config for the given instance ID. + Get(instanceID string) (*v1.RemoteConfig, error) + // Update or create a remote config for the given instance ID. + Update(instanceID string, config *v1.RemoteConfig) error + // Delete a remote config for the given instance ID. + Delete(instanceID string) error +} + +type jsonDirRemoteConfigStore struct { + mu sync.Mutex + dir string + cache map[string]*v1.RemoteConfig +} + +func NewJSONDirRemoteConfigStore(dir string) RemoteConfigStore { + return &jsonDirRemoteConfigStore{ + dir: dir, + cache: make(map[string]*v1.RemoteConfig), + } +} + +func (s *jsonDirRemoteConfigStore) Get(instanceID string) (*v1.RemoteConfig, error) { + s.mu.Lock() + defer s.mu.Unlock() + if instanceID == "" { + return nil, errors.New("instanceID is required") + } + + if config, ok := s.cache[instanceID]; ok { + return config, nil + } + + file := s.fileForInstance(instanceID) + data, err := os.ReadFile(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, ErrRemoteConfigNotFound + } + return nil, fmt.Errorf("read config file: %w", err) + } + + var config v1.RemoteConfig + if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("unmarshal config: %w", err) + } + + s.cache[instanceID] = &config + return &config, nil +} + +func (s *jsonDirRemoteConfigStore) Update(instanceID string, config *v1.RemoteConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + if instanceID == "" { + return errors.New("instanceID is required") + } + + file := s.fileForInstance(instanceID) + data, err := protojson.MarshalOptions{ + Indent: " ", + Multiline: true, + }.Marshal(config) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + err = os.MkdirAll(filepath.Dir(file), 0755) + if err != nil { + return fmt.Errorf("create config directory: %w", err) + } + + err = os.WriteFile(file, data, 0600) + if err != nil { + return fmt.Errorf("write config file: %w", err) + } + + s.cache[instanceID] = config + return nil +} + +func (s *jsonDirRemoteConfigStore) Delete(instanceID string) error { + s.mu.Lock() + defer s.mu.Unlock() + if instanceID == "" { + return errors.New("instanceID is required") + } + + file := s.fileForInstance(instanceID) + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("remove config file: %w", err) + } + + delete(s.cache, instanceID) + return nil +} + +func (s *jsonDirRemoteConfigStore) fileForInstance(instanceID string) string { + safeInstanceID := strings.Replace(instanceID, "..", ".", -1) + safeInstanceID = sanitizeFilenameRegex.ReplaceAllString(safeInstanceID, "_") + checksum := crc32.ChecksumIEEE([]byte(instanceID)) // checksum eliminates collisions in the case of replacing characters. + return filepath.Join(s.dir, fmt.Sprintf("%s-%08x.json", safeInstanceID, checksum)) +} + +type memoryConfigStore struct { + configs map[string]*v1.RemoteConfig +} + +func newMemoryConfigStore() *memoryConfigStore { + return &memoryConfigStore{ + configs: make(map[string]*v1.RemoteConfig), + } +} + +func (s *memoryConfigStore) Get(instanceID string) (*v1.RemoteConfig, error) { + if config, ok := s.configs[instanceID]; ok { + return config, nil + } + return nil, ErrRemoteConfigNotFound +} + +func (s *memoryConfigStore) Update(instanceID string, config *v1.RemoteConfig) error { + if instanceID == "" { + return errors.New("instanceID is required") + } + s.configs[instanceID] = config + return nil +} + +func (s *memoryConfigStore) Delete(instanceID string) error { + if instanceID == "" { + return errors.New("instanceID is required") + } + delete(s.configs, instanceID) + return nil +} + +func GetRepoConfig(store RemoteConfigStore, instanceID, repoID string) (*v1.RemoteRepo, error) { + config, err := store.Get(instanceID) + if err != nil { + return nil, fmt.Errorf("get %q: %w", instanceID, err) + } + for _, repo := range config.Repos { + if repo.Id == repoID { + return repo, nil + } + } + return nil, fmt.Errorf("get %q/%q: %w", instanceID, repoID, ErrRemoteConfigNotFound) +} diff --git a/internal/api/syncapi/syncapi_test.go b/internal/api/syncapi/syncapi_test.go new file mode 100644 index 000000000..4ed00b6fa --- /dev/null +++ b/internal/api/syncapi/syncapi_test.go @@ -0,0 +1,656 @@ +package syncapi + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "path/filepath" + "slices" + "sync" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" + "github.com/garethgeorge/backrest/internal/orchestrator" + "github.com/garethgeorge/backrest/internal/resticinstaller" + "github.com/garethgeorge/backrest/internal/testutil" + "github.com/google/go-cmp/cmp" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +const ( + defaultClientID = "test-client" + defaultHostID = "test-host" + defaultRepoID = "test-repo" + defaultPlanID = "test-plan" +) + +var ( + defaultRepoGUID = cryptoutil.MustRandomID(cryptoutil.DefaultIDBits) +) + +var ( + basicHostOperationTempl = &v1.Operation{ + InstanceId: defaultHostID, + RepoId: defaultRepoID, + RepoGuid: defaultRepoGUID, + PlanId: defaultPlanID, + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + Status: v1.OperationStatus_STATUS_SUCCESS, + Op: &v1.Operation_OperationBackup{}, + } + + basicClientOperationTempl = &v1.Operation{ + InstanceId: defaultClientID, + RepoId: defaultRepoID, + RepoGuid: defaultRepoGUID, + PlanId: defaultPlanID, + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + Status: v1.OperationStatus_STATUS_SUCCESS, + Op: &v1.Operation_OperationBackup{}, + } +) + +func TestConnectionSucceeds(t *testing.T) { + testutil.InstallZapLogger(t) + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + + peerHostAddr := allocBindAddrForTest(t) + peerClientAddr := allocBindAddrForTest(t) + + peerHostConfig := &v1.Config{ + Instance: defaultHostID, + Repos: []*v1.Repo{}, + Multihost: &v1.Multihost{ + AuthorizedClients: []*v1.Multihost_Peer{ + { + InstanceId: defaultClientID, + }, + }, + }, + } + + peerClientConfig := &v1.Config{ + Instance: defaultClientID, + Repos: []*v1.Repo{}, + Multihost: &v1.Multihost{ + KnownHosts: []*v1.Multihost_Peer{ + { + InstanceId: defaultHostID, + InstanceUrl: fmt.Sprintf("http://%s", peerHostAddr), + }, + }, + }, + } + + peerHost := newPeerUnderTest(t, peerHostConfig) + peerClient := newPeerUnderTest(t, peerClientConfig) + + startRunningSyncAPI(t, peerHost, peerHostAddr) + startRunningSyncAPI(t, peerClient, peerClientAddr) + + tryConnect(t, ctx, peerClient, defaultHostID) +} + +func TestSyncConfigChange(t *testing.T) { + testutil.InstallZapLogger(t) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + + peerHostAddr := allocBindAddrForTest(t) + peerClientAddr := allocBindAddrForTest(t) + + peerHostConfig := &v1.Config{ + Instance: defaultHostID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + AllowedPeerInstanceIds: []string{defaultClientID}, + }, + { + Id: "do-not-sync", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + AllowedPeerInstanceIds: []string{"some-other-client"}, + }, + }, + Multihost: &v1.Multihost{ + AuthorizedClients: []*v1.Multihost_Peer{ + { + InstanceId: defaultClientID, + }, + }, + }, + } + + peerClientConfig := &v1.Config{ + Instance: defaultClientID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + Uri: "backrest://" + defaultHostID, // TODO: get rid of the :// requirement + }, + }, + Multihost: &v1.Multihost{ + KnownHosts: []*v1.Multihost_Peer{ + { + InstanceId: defaultHostID, + InstanceUrl: fmt.Sprintf("http://%s", peerHostAddr), + }, + }, + }, + } + + peerHost := newPeerUnderTest(t, peerHostConfig) + peerClient := newPeerUnderTest(t, peerClientConfig) + + startRunningSyncAPI(t, peerHost, peerHostAddr) + startRunningSyncAPI(t, peerClient, peerClientAddr) + + tryConnect(t, ctx, peerClient, defaultHostID) + + // wait for the initial config to propagate + tryExpectConfig(t, ctx, peerClient, defaultHostID, &v1.RemoteConfig{ + Repos: []*v1.RemoteRepo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + }, + }, + }) + hostConfigChanged := proto.Clone(peerHostConfig).(*v1.Config) + hostConfigChanged.Repos[0].Env = []string{"SOME_ENV=VALUE"} + peerHost.configMgr.Update(hostConfigChanged) + + tryExpectConfig(t, ctx, peerClient, defaultHostID, &v1.RemoteConfig{ + Repos: []*v1.RemoteRepo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + Env: []string{"SOME_ENV=VALUE"}, + }, + }, + }) +} + +func TestSimpleOperationSync(t *testing.T) { + testutil.InstallZapLogger(t) + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + + peerHostAddr := allocBindAddrForTest(t) + peerClientAddr := allocBindAddrForTest(t) + + peerHostConfig := &v1.Config{ + Instance: defaultHostID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + AllowedPeerInstanceIds: []string{defaultClientID}, + }, + }, + Multihost: &v1.Multihost{ + AuthorizedClients: []*v1.Multihost_Peer{ + { + InstanceId: defaultClientID, + }, + }, + }, + } + + peerClientConfig := &v1.Config{ + Instance: defaultClientID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + Uri: "backrest://" + defaultHostID, // TODO: get rid of the :// requirement + }, + }, + Multihost: &v1.Multihost{ + KnownHosts: []*v1.Multihost_Peer{ + { + InstanceId: defaultHostID, + InstanceUrl: fmt.Sprintf("http://%s", peerHostAddr), + }, + }, + }, + } + + peerHost := newPeerUnderTest(t, peerHostConfig) + peerClient := newPeerUnderTest(t, peerClientConfig) + + peerHost.oplog.Add(testutil.OperationsWithDefaults(basicHostOperationTempl, []*v1.Operation{ + { + DisplayMessage: "hostop1", + }, + })...) + peerHost.oplog.Add(testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + DisplayMessage: "clientop-missing", + OriginalId: 1234, // must be an ID that doesn't exist remotely + }, + })...) + + if err := peerClient.oplog.Add(testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + DisplayMessage: "clientop1", + FlowId: 1, + }, + { + DisplayMessage: "clientop2", + FlowId: 1, + }, + { + DisplayMessage: "clientop3", + FlowId: 2, // in a different flow from the other two + }, + })...); err != nil { + t.Fatalf("failed to add operations: %v", err) + } + + startRunningSyncAPI(t, peerHost, peerHostAddr) + startRunningSyncAPI(t, peerClient, peerClientAddr) + + tryConnect(t, ctx, peerClient, defaultHostID) + + tryExpectOperationsSynced(t, ctx, peerHost, peerClient, oplog.Query{}.SetInstanceID(defaultClientID).SetRepoGUID(defaultRepoGUID), "host and client should be synced") + tryExpectExactOperations(t, ctx, peerHost, oplog.Query{}.SetInstanceID(defaultClientID).SetRepoGUID(defaultRepoGUID), + testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + Id: 3, // b/c of the already inserted host ops the sync'd ops start at 3 + FlowId: 3, + OriginalId: 1, + OriginalFlowId: 1, + DisplayMessage: "clientop1", + }, + { + Id: 4, + FlowId: 3, + OriginalId: 2, + OriginalFlowId: 1, + DisplayMessage: "clientop2", + }, + { + Id: 5, + FlowId: 5, + OriginalId: 3, + OriginalFlowId: 2, + DisplayMessage: "clientop3", + }, + }), "host and client should be synced") +} + +func TestSyncMutations(t *testing.T) { + testutil.InstallZapLogger(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + peerHostAddr := allocBindAddrForTest(t) + peerClientAddr := allocBindAddrForTest(t) + + peerHostConfig := &v1.Config{ + Instance: defaultHostID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + AllowedPeerInstanceIds: []string{defaultClientID}, + }, + }, + Multihost: &v1.Multihost{ + AuthorizedClients: []*v1.Multihost_Peer{ + { + InstanceId: defaultClientID, + }, + }, + }, + } + + peerClientConfig := &v1.Config{ + Instance: defaultClientID, + Repos: []*v1.Repo{ + { + Id: defaultRepoID, + Guid: defaultRepoGUID, + Uri: "backrest://" + defaultHostID, // TODO: get rid of the :// requirement + }, + }, + Multihost: &v1.Multihost{ + KnownHosts: []*v1.Multihost_Peer{ + { + InstanceId: defaultHostID, + InstanceUrl: fmt.Sprintf("http://%s", peerHostAddr), + }, + }, + }, + } + + peerHost := newPeerUnderTest(t, peerHostConfig) + peerClient := newPeerUnderTest(t, peerClientConfig) + + op := testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + DisplayMessage: "clientop1", + }, + })[0] + + if err := peerClient.oplog.Add(op); err != nil { + t.Fatalf("failed to add operations: %v", err) + } + + syncCtx, cancelSyncCtx := context.WithCancel(ctx) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + runSyncAPIWithCtx(syncCtx, peerHost, peerHostAddr) + }() + go func() { + defer wg.Done() + runSyncAPIWithCtx(syncCtx, peerClient, peerClientAddr) + }() + tryConnect(t, ctx, peerClient, defaultHostID) + + tryExpectOperationsSynced(t, ctx, peerClient, peerHost, oplog.Query{}.SetRepoGUID(defaultRepoGUID), "host and client should sync initially") + + op.DisplayMessage = "clientop1-mod-while-online" + if err := peerClient.oplog.Update(op); err != nil { + t.Fatalf("failed to update operation: %v", err) + } + + tryExpectExactOperations(t, ctx, peerHost, oplog.Query{}.SetRepoGUID(defaultRepoGUID), + testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + Id: 1, + DisplayMessage: "clientop1-mod-while-online", + OriginalFlowId: 1, + OriginalId: 1, + FlowId: 1, + }, + }), "host and client should sync online edits") + + // Wait for shutdown + cancelSyncCtx() + wg.Wait() + + // Now make an offline edit + op.DisplayMessage = "clientop1-mod-while-offline" + if err := peerClient.oplog.Update(op); err != nil { + t.Fatalf("failed to add operations: %v", err) + } + + // Now restart sync and check that the offline edit is applied + syncCtx, cancelSyncCtx = context.WithCancel(ctx) + wg.Add(2) + go func() { + defer wg.Done() + runSyncAPIWithCtx(syncCtx, peerHost, peerHostAddr) + }() + + go func() { + defer wg.Done() + runSyncAPIWithCtx(syncCtx, peerClient, peerClientAddr) + }() + tryConnect(t, ctx, peerClient, defaultHostID) + + // Verify all operations are synced after reconnection + tryExpectExactOperations(t, ctx, peerHost, oplog.Query{}.SetRepoGUID(defaultRepoGUID), + testutil.OperationsWithDefaults(basicClientOperationTempl, []*v1.Operation{ + { + Id: 1, + DisplayMessage: "clientop1-mod-while-offline", + OriginalFlowId: 1, + OriginalId: 1, + FlowId: 1, + }, + }), "host and client should sync offline edits") + + // Clean up + cancelSyncCtx() + wg.Wait() +} + +func getOperations(t *testing.T, oplog *oplog.OpLog, query oplog.Query) []*v1.Operation { + ops := []*v1.Operation{} + if err := oplog.Query(query, func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); err != nil { + t.Fatalf("failed to get operations: %v", err) + } + return ops +} + +func tryExpectExactOperations(t *testing.T, ctx context.Context, peer *peerUnderTest, query oplog.Query, wantOps []*v1.Operation, message string) { + err := testutil.Retry(t, ctx, func() error { + ops := getOperations(t, peer.oplog, query) + for _, op := range ops { + op.Modno = 0 + } + if diff := cmp.Diff(ops, wantOps, protocmp.Transform()); diff != "" { + return fmt.Errorf("unexpected diff: %v", diff) + } + return nil + }) + if err != nil { + opsJson, _ := protojson.MarshalOptions{Indent: " "}.Marshal(&v1.OperationList{Operations: getOperations(t, peer.oplog, query)}) + t.Logf("found operations: %v", string(opsJson)) + t.Fatalf("%v: timeout without finding wanted operations: %v", message, err) + } +} + +func tryExpectOperationsSynced(t *testing.T, ctx context.Context, peer1 *peerUnderTest, peer2 *peerUnderTest, query oplog.Query, message string) { + err := testutil.Retry(t, ctx, func() error { + peer1Ops := getOperations(t, peer1.oplog, query) + peer2Ops := getOperations(t, peer2.oplog, query) + // clear fields that we expect will be re-mapped + for _, op := range peer1Ops { + op.Id = 0 + op.FlowId = 0 + op.OriginalId = 0 + op.OriginalFlowId = 0 + } + for _, op := range peer2Ops { + op.Id = 0 + op.FlowId = 0 + op.OriginalId = 0 + op.OriginalFlowId = 0 + } + + sortFn := func(a, b *v1.Operation) int { + if a.DisplayMessage < b.DisplayMessage { + return -1 + } + return 1 + } + + slices.SortFunc(peer1Ops, sortFn) + slices.SortFunc(peer2Ops, sortFn) + + if len(peer1Ops) == 0 { + return errors.New("no operations found in peer1") + } + if len(peer2Ops) == 0 { + return errors.New("no operations found in peer2") + } + if diff := cmp.Diff(peer1Ops, peer2Ops, protocmp.Transform()); diff != "" { + return fmt.Errorf("unexpected diff: %v", diff) + } + + return nil + }) + if err != nil { + ops1Json, _ := protojson.MarshalOptions{Indent: " "}.Marshal(&v1.OperationList{Operations: getOperations(t, peer1.oplog, query)}) + ops2Json, _ := protojson.MarshalOptions{Indent: " "}.Marshal(&v1.OperationList{Operations: getOperations(t, peer2.oplog, query)}) + t.Logf("peer1 operations: %v", string(ops1Json)) + t.Logf("peer2 operations: %v", string(ops2Json)) + t.Fatalf("timeout without syncing operations: %v", err) + } +} + +func tryExpectConfig(t *testing.T, ctx context.Context, peer *peerUnderTest, instanceID string, wantCfg *v1.RemoteConfig) { + testutil.Try(t, ctx, func() error { + cfg, err := peer.manager.remoteConfigStore.Get(instanceID) + if err != nil { + return err + } + if diff := cmp.Diff(cfg, wantCfg, protocmp.Transform()); diff != "" { + return fmt.Errorf("unexpected diff: %v", diff) + } + return nil + }) +} + +func tryConnect(t *testing.T, ctx context.Context, peer *peerUnderTest, instanceID string) { + testutil.Try(t, ctx, func() error { + allClients := peer.manager.GetSyncClients() + client, ok := allClients[instanceID] + if !ok { + return fmt.Errorf("client not found, got %v", allClients) + } + state, _ := client.GetConnectionState() + if state != v1.SyncConnectionState_CONNECTION_STATE_CONNECTED { + return fmt.Errorf("expected connection state to be CONNECTED, got %v", v1.SyncConnectionState.String(state)) + } + return nil + }) +} + +func allocBindAddrForTest(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer listener.Close() + + // Get the port number from the listener + _, port, err := net.SplitHostPort(listener.Addr().String()) + if err != nil { + t.Fatalf("failed to split host and port: %v", err) + } + + return "127.0.0.1:" + port +} + +func runSyncAPIWithCtx(ctx context.Context, peer *peerUnderTest, bindAddr string) { + mux := http.NewServeMux() + syncHandler := NewBackrestSyncHandler(peer.manager) + mux.Handle(v1connect.NewBackrestSyncServiceHandler(syncHandler)) + + server := &http.Server{ + Addr: bindAddr, + Handler: h2c.NewHandler(mux, &http2.Server{}), // h2c is HTTP/2 without TLS for grpc-connect support. + } + + var wg sync.WaitGroup + + go func() { + <-ctx.Done() + server.Shutdown(context.Background()) + }() + + wg.Add(1) + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + wg.Done() + }() + + wg.Add(1) + go func() { + peer.manager.RunSync(ctx) + wg.Done() + }() + + wg.Wait() +} + +func startRunningSyncAPI(t *testing.T, peer *peerUnderTest, bindAddr string) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + go runSyncAPIWithCtx(ctx, peer, bindAddr) +} + +type peerUnderTest struct { + manager *SyncManager + oplog *oplog.OpLog + opstore oplog.OpStore + configMgr *config.ConfigManager +} + +func newPeerUnderTest(t *testing.T, initialConfig *v1.Config) *peerUnderTest { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + + configMgr := &config.ConfigManager{Store: &config.MemoryStore{Config: initialConfig}} + opstore, err := sqlitestore.NewMemorySqliteStore() + t.Cleanup(func() { opstore.Close() }) + if err != nil { + t.Fatalf("failed to create opstore: %v", err) + } + oplog, err := oplog.NewOpLog(opstore) + if err != nil { + t.Fatalf("failed to create oplog: %v", err) + } + + resticbin, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + t.Fatalf("failed to find or install restic binary: %v", err) + } + + tempDir := t.TempDir() + logStore, err := logstore.NewLogStore(filepath.Join(tempDir, "tasklogs")) + t.Cleanup(func() { logStore.Close() }) + if err != nil { + t.Fatalf("failed to create log store: %v", err) + } + + var wg sync.WaitGroup + orchestrator, err := orchestrator.NewOrchestrator(resticbin, configMgr, oplog, logStore) + if err != nil { + t.Fatalf("failed to create orchestrator: %v", err) + } + wg.Add(1) + go func() { + orchestrator.Run(ctx) + wg.Done() + }() + + t.Cleanup(func() { + cancel() + wg.Wait() + }) + + remoteConfigStore := NewJSONDirRemoteConfigStore(filepath.Join(tempDir, "remoteconfig")) + + manager := NewSyncManager(configMgr, remoteConfigStore, oplog, orchestrator) + manager.syncClientRetryDelay = 250 * time.Millisecond + + return &peerUnderTest{ + manager: manager, + oplog: oplog, + opstore: opstore, + configMgr: configMgr, + } +} diff --git a/internal/api/syncapi/syncclient.go b/internal/api/syncapi/syncclient.go new file mode 100644 index 000000000..4264536f2 --- /dev/null +++ b/internal/api/syncapi/syncclient.go @@ -0,0 +1,441 @@ +package syncapi + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "sync" + "time" + + "connectrpc.com/connect" + "github.com/garethgeorge/backrest/gen/go/types" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.uber.org/zap" + "golang.org/x/net/http2" + "google.golang.org/protobuf/proto" +) + +type SyncClient struct { + mgr *SyncManager + localInstanceID string + peer *v1.Multihost_Peer + oplog *oplog.OpLog + client v1connect.BackrestSyncServiceClient + reconnectDelay time.Duration + l *zap.Logger + + // mutable properties + mu sync.Mutex + connectionStatus v1.SyncConnectionState + connectionStatusMessage string +} + +func newInsecureClient() *http.Client { + return &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + IdleConnTimeout: 300 * time.Second, + ReadIdleTimeout: 60 * time.Second, + }, + } +} + +func NewSyncClient(mgr *SyncManager, localInstanceID string, peer *v1.Multihost_Peer, oplog *oplog.OpLog) (*SyncClient, error) { + if peer.GetInstanceUrl() == "" { + return nil, errors.New("peer instance URL is required") + } + + client := v1connect.NewBackrestSyncServiceClient( + newInsecureClient(), + peer.GetInstanceUrl(), + ) + + c := &SyncClient{ + mgr: mgr, + localInstanceID: localInstanceID, + peer: peer, + reconnectDelay: mgr.syncClientRetryDelay, + client: client, + oplog: oplog, + l: zap.L().Named(fmt.Sprintf("syncclient for %q", peer.GetInstanceId())), + } + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_DISCONNECTED, "starting up") + return c, nil +} + +func (c *SyncClient) setConnectionState(state v1.SyncConnectionState, message string) { + c.mu.Lock() + c.connectionStatus = state + c.connectionStatusMessage = message + c.mu.Unlock() +} + +func (c *SyncClient) GetConnectionState() (v1.SyncConnectionState, string) { + c.mu.Lock() + defer c.mu.Unlock() + return c.connectionStatus, c.connectionStatusMessage +} + +func (c *SyncClient) RunSync(ctx context.Context) { + for { + if ctx.Err() != nil { + return + } + + lastConnect := time.Now() + + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_PENDING, "connection pending") + + if err := c.runSyncInternal(ctx); err != nil { + c.l.Sugar().Errorf("sync error: %v", err) + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_DISCONNECTED, err.Error()) + } + + delay := c.reconnectDelay - time.Since(lastConnect) + c.l.Sugar().Infof("disconnected, will retry after %v", delay) + select { + case <-time.After(delay): + case <-ctx.Done(): + return + } + } +} + +func (c *SyncClient) runSyncInternal(ctx context.Context) error { + c.l.Info("connecting to sync server") + stream := c.client.Sync(ctx) + + ctx, cancelWithError := context.WithCancelCause(ctx) + defer cancelWithError(nil) + + receiveError := make(chan error, 1) + receive := make(chan *v1.SyncStreamItem, 1) + send := make(chan *v1.SyncStreamItem, 100) + + go func() { + for { + item, err := stream.Receive() + if err != nil { + receiveError <- err + return + } + receive <- item + } + }() + + // Broadcast initial packet containing the protocol version and instance ID. + // TODO: do this in a header instead of as a part of the stream. + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_Handshake{ + Handshake: &v1.SyncStreamItem_SyncActionHandshake{ + ProtocolVersion: SyncProtocolVersion, + InstanceId: &v1.SignedMessage{ + Payload: []byte(c.localInstanceID), + Signature: []byte("TOOD: inject a valid signature"), + Keyid: "TODO: inject a valid key ID", + }, + }, + }, + }); err != nil { + // note: the error checking w/streams in connectrpc is fairly awkward. + // If write returns an EOF error, we are expected to call stream.Receive() + // to get the unmarshalled network failure. + if !errors.Is(err, io.EOF) { + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_ERROR_PROTOCOL, err.Error()) + return err + } else { + _, err2 := stream.Receive() + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_DISCONNECTED, err.Error()) + return err2 + } + } + c.setConnectionState(v1.SyncConnectionState_CONNECTION_STATE_CONNECTED, "connected") + + // Wait for the handshake packet from the server. + serverInstanceID := "" + if msg, ok := <-receive; ok { + handshake := msg.GetHandshake() + if handshake == nil { + return connect.NewError(connect.CodeInvalidArgument, errors.New("handshake packet must be sent first")) + } + + serverInstanceID = string(handshake.GetInstanceId().GetPayload()) + if serverInstanceID == "" { + return connect.NewError(connect.CodeInvalidArgument, errors.New("instance ID is required")) + } + + if handshake.GetProtocolVersion() != SyncProtocolVersion { + return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("unsupported peer protocol version, got %d, expected %d", handshake.GetProtocolVersion(), SyncProtocolVersion)) + } + } else { + return connect.NewError(connect.CodeInvalidArgument, errors.New("no packets received")) + } + + if serverInstanceID != c.peer.InstanceId { + return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("server instance ID %q does not match expected peer instance ID %q", serverInstanceID, c.peer.InstanceId)) + } + + // haveRunSync tracks which repo GUIDs we've initiated a sync for with the server. + // operation requests (from the server) are ignored if the GUID is not allowlisted in this map. + haveRunSync := make(map[string]struct{}) + + oplogSubscription := func(ops []*v1.Operation, event oplog.OperationEvent) { + var opsToForward []*v1.Operation + for _, op := range ops { + if _, ok := haveRunSync[op.GetRepoGuid()]; ok { + opsToForward = append(opsToForward, op) + } + } + + if len(opsToForward) == 0 { + return + } + + var eventProto *v1.OperationEvent + if event == oplog.OPERATION_ADDED { + eventProto = &v1.OperationEvent{ + Event: &v1.OperationEvent_CreatedOperations{ + CreatedOperations: &v1.OperationList{Operations: opsToForward}, + }, + } + } else if event == oplog.OPERATION_UPDATED { + eventProto = &v1.OperationEvent{ + Event: &v1.OperationEvent_UpdatedOperations{ + UpdatedOperations: &v1.OperationList{Operations: opsToForward}, + }, + } + } else if event == oplog.OPERATION_DELETED { + ids := make([]int64, len(opsToForward)) + for i, op := range opsToForward { + ids[i] = op.GetId() + } + eventProto = &v1.OperationEvent{ + Event: &v1.OperationEvent_DeletedOperations{ + DeletedOperations: &types.Int64List{Values: ids}, + }, + } + } + + select { + case send <- &v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_SendOperations{ + SendOperations: &v1.SyncStreamItem_SyncActionSendOperations{ + Event: eventProto, + }, + }, + }: + default: + cancelWithError(fmt.Errorf("operation send buffer overflow")) + } + } + c.oplog.Subscribe(oplog.Query{}, &oplogSubscription) + defer c.oplog.Unsubscribe(&oplogSubscription) + + handleSyncCommand := func(item *v1.SyncStreamItem) error { + switch action := item.Action.(type) { + case *v1.SyncStreamItem_SendConfig: + c.l.Sugar().Debugf("received remote config update") + newRemoteConfig := action.SendConfig.Config + if err := c.mgr.remoteConfigStore.Update(c.peer.InstanceId, newRemoteConfig); err != nil { + return fmt.Errorf("update remote config store with latest config: %w", err) + } + + if newRemoteConfig == nil { + return fmt.Errorf("received nil remote config") + } + + // remove any repo IDs that are no longer in the config, our access has been revoked. + remoteRepoGUIDs := make(map[string]struct{}) + for _, repo := range newRemoteConfig.Repos { + remoteRepoGUIDs[repo.GetGuid()] = struct{}{} + } + for repoID := range haveRunSync { + if _, ok := remoteRepoGUIDs[repoID]; !ok { + delete(haveRunSync, repoID) + } + } + + // load the local config so that we can index the remote repos into any local repos that reference their URIs + // e.g. backrest: format URI. + localConfig, err := c.mgr.configMgr.Get() + if err != nil { + return fmt.Errorf("get local config: %w", err) + } + + for _, repo := range newRemoteConfig.Repos { + _, ok := haveRunSync[repo.GetGuid()] + if ok { + continue + } + localRepoConfig := config.FindRepoByGUID(localConfig, repo.GetGuid()) + if localRepoConfig == nil { + c.l.Sugar().Debugf("ignoring remote repo config %q/%q because no local repo has the same GUID %q", c.peer.InstanceId, repo.GetId()) + continue + } + instanceID, err := InstanceForBackrestURI(localRepoConfig.Uri) + if err != nil || instanceID != c.peer.InstanceId { + c.l.Sugar().Debugf("ignoring remote repo config %q/%q because the local repo (%q) with the same GUID specifies URI %q (instance ID %q) which does not reference the peer providing this config", c.peer.InstanceId, repo.GetId(), localRepoConfig.Id, localRepoConfig.Guid, instanceID) + continue + } + + diffSel := &v1.OpSelector{ + InstanceId: proto.String(c.localInstanceID), + RepoGuid: proto.String(repo.GetGuid()), + } + + diffQuery, err := protoutil.OpSelectorToQuery(diffSel) + if err != nil { + return fmt.Errorf("convert operation selector to query: %w", err) + } + + haveRunSync[repo.GetGuid()] = struct{}{} + + // Load operation metadata and send the initial diff state. + var opIds []int64 + var opModnos []int64 + if err := c.oplog.QueryMetadata(diffQuery, func(op oplog.OpMetadata) error { + opIds = append(opIds, op.ID) + opModnos = append(opModnos, op.Modno) + return nil + }); err != nil { + return fmt.Errorf("action sync config: query oplog for repo %q: %w", repo.GetId(), err) + } + + c.l.Sugar().Infof("initiating operation history sync for repo %q", repo.GetId()) + + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_DiffOperations{ + DiffOperations: &v1.SyncStreamItem_SyncActionDiffOperations{ + HaveOperationsSelector: diffSel, + HaveOperationIds: opIds, + HaveOperationModnos: opModnos, + }, + }, + }); err != nil { + return fmt.Errorf("action sync config: send diff operations: %w", err) + } + } + case *v1.SyncStreamItem_DiffOperations: + requestedOperations := action.DiffOperations.GetRequestOperations() + c.l.Sugar().Debugf("received operation request for operations: %v", requestedOperations) + + var deletedIDs []int64 + var sendOps []*v1.Operation + + sendOpsFunc := func() error { + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_SendOperations{ + SendOperations: &v1.SyncStreamItem_SyncActionSendOperations{ + Event: &v1.OperationEvent{ + Event: &v1.OperationEvent_CreatedOperations{ + CreatedOperations: &v1.OperationList{Operations: sendOps}, + }, + }, + }, + }, + }); err != nil { + sendOps = sendOps[:0] + return fmt.Errorf("action diff operations: send create operations: %w", err) + } + c.l.Sugar().Debugf("sent %d operations", len(sendOps)) + sendOps = sendOps[:0] + return nil + } + + sentOps := 0 + for _, opID := range requestedOperations { + op, err := c.oplog.Get(opID) + if err != nil { + if errors.Is(err, oplog.ErrNotExist) { + deletedIDs = append(deletedIDs, opID) + continue + } + c.l.Sugar().Warnf("action diff operations, failed to fetch a requested operation %d: %v", opID, err) + continue // skip this operation + } + if op.GetInstanceId() != c.localInstanceID { + c.l.Sugar().Warnf("action diff operations, requested operation %d is not from this instance, this shouldn't happen with a wellbehaved server", opID) + continue // skip operations that are not from this instance e.g. an "index snapshot" picking up snapshots created by another instance. + } + + _, ok := haveRunSync[op.RepoGuid] + if !ok { + // this should never happen if sync is working correctly. Would probably indicate oplog or our access was revoked. + // Error out and re-initiate sync. + return fmt.Errorf("remote requested operation for repo %q for which sync was never initiated", op.GetRepoId()) + } + + sendOps = append(sendOps, op) + sentOps += 1 + if len(sendOps) >= 256 { + if err := sendOpsFunc(); err != nil { + return err + } + } + } + + if len(sendOps) > 0 { + if err := sendOpsFunc(); err != nil { + return err + } + } + + if len(deletedIDs) > 0 { + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_SendOperations{ + SendOperations: &v1.SyncStreamItem_SyncActionSendOperations{ + Event: &v1.OperationEvent{ + Event: &v1.OperationEvent_DeletedOperations{ + DeletedOperations: &types.Int64List{Values: deletedIDs}, + }, + }, + }, + }, + }); err != nil { + return fmt.Errorf("action diff operations: send delete operations: %w", err) + } + } + + c.l.Debug("replied to an operations request", zap.Int("num_ops_requested", len(requestedOperations)), zap.Int("num_ops_sent", sentOps), zap.Int("num_ops_deleted", len(deletedIDs))) + case *v1.SyncStreamItem_Throttle: + c.reconnectDelay = time.Duration(action.Throttle.GetDelayMs()) * time.Millisecond + default: + return fmt.Errorf("unknown action: %v", action) + } + return nil + } + + for { + select { + case err := <-receiveError: + return fmt.Errorf("connection terminated with error: %w", err) + case item, ok := <-receive: + if !ok { + return nil + } + if err := handleSyncCommand(item); err != nil { + return err + } + case sendItem, ok := <-send: // note: send channel should only be used when sending from a different goroutine than the main loop + if !ok { + return nil + } + if err := stream.Send(sendItem); err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/internal/api/syncapi/synchandler.go b/internal/api/syncapi/synchandler.go new file mode 100644 index 000000000..317fb763a --- /dev/null +++ b/internal/api/syncapi/synchandler.go @@ -0,0 +1,403 @@ +package syncapi + +import ( + "context" + "errors" + "fmt" + "slices" + "sort" + + "connectrpc.com/connect" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/gen/go/v1/v1connect" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + lru "github.com/hashicorp/golang-lru/v2" + "go.uber.org/zap" +) + +const SyncProtocolVersion = 1 + +type BackrestSyncHandler struct { + v1connect.UnimplementedBackrestSyncServiceHandler + mgr *SyncManager +} + +var _ v1connect.BackrestSyncServiceHandler = &BackrestSyncHandler{} + +func NewBackrestSyncHandler(mgr *SyncManager) *BackrestSyncHandler { + return &BackrestSyncHandler{ + mgr: mgr, + } +} + +func (h *BackrestSyncHandler) Sync(ctx context.Context, stream *connect.BidiStream[v1.SyncStreamItem, v1.SyncStreamItem]) error { + // TODO: this request can be very long lived, we must periodically refresh the config + // e.g. to disconnect a client if its access is revoked. + initialConfig, err := h.mgr.configMgr.Get() + if err != nil { + return err + } + + receive := make(chan *v1.SyncStreamItem, 1) + send := make(chan *v1.SyncStreamItem, 1) + go func() { + for { + item, err := stream.Receive() + if err != nil { + break + } + receive <- item + } + close(receive) + }() + + // Broadcast initial packet containing the protocol version and instance ID. + zap.S().Debugf("syncserver a client connected, broadcast handshake as %v", initialConfig.Instance) + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_Handshake{ + Handshake: &v1.SyncStreamItem_SyncActionHandshake{ + ProtocolVersion: SyncProtocolVersion, + InstanceId: &v1.SignedMessage{ + Payload: []byte(initialConfig.Instance), + Signature: []byte("TODO: inject a valid signature"), + Keyid: "TODO: inject a valid key ID", + }, + }, + }, + }); err != nil { + return err + } + + // Try to read the handshake packet from the client. + // TODO: perform this handshake in a header as a pre-flight before opening the stream. + clientInstanceID := "" + if msg, ok := <-receive; ok { + handshake := msg.GetHandshake() + if handshake == nil { + return connect.NewError(connect.CodeInvalidArgument, errors.New("handshake packet must be sent first")) + } + + clientInstanceID = string(handshake.GetInstanceId().GetPayload()) + if clientInstanceID == "" { + return connect.NewError(connect.CodeInvalidArgument, errors.New("instance ID is required")) + } + } else { + return connect.NewError(connect.CodeInvalidArgument, errors.New("no packets received")) + } + + var authorizedClientPeer *v1.Multihost_Peer + authorizedClientPeerIdx := slices.IndexFunc(initialConfig.Multihost.GetAuthorizedClients(), func(peer *v1.Multihost_Peer) bool { + return peer.InstanceId == clientInstanceID + }) + if authorizedClientPeerIdx == -1 { + // TODO: check the key signature of the handshake message here. + zap.S().Warnf("syncserver rejected a connection from client instance ID %q because it is not authorized", clientInstanceID) + return connect.NewError(connect.CodePermissionDenied, errors.New("client is not an authorized peer")) + } else { + authorizedClientPeer = initialConfig.Multihost.AuthorizedClients[authorizedClientPeerIdx] + } + zap.S().Infof("syncserver accepted a connection from client instance ID %q", authorizedClientPeer.InstanceId) + + opIDLru, _ := lru.New[int64, int64](2048) // original ID -> local ID + flowIDLru, _ := lru.New[int64, int64](2048) // original flow ID -> local flow ID + + insertOrUpdate := func(op *v1.Operation) error { + op.OriginalId = op.Id + op.OriginalFlowId = op.FlowId + var ok bool + if op.Id, ok = opIDLru.Get(op.OriginalId); !ok { + var foundOp *v1.Operation + if err := h.mgr.oplog.Query(oplog.Query{}. + SetOriginalID(op.OriginalId). + SetInstanceID(op.InstanceId), func(o *v1.Operation) error { + foundOp = o + return nil + }); err != nil { + return fmt.Errorf("mapping remote ID to local ID: %w", err) + } + if foundOp != nil { + op.Id = foundOp.Id + opIDLru.Add(foundOp.Id, foundOp.Id) + } + } + if op.FlowId, ok = flowIDLru.Get(op.OriginalFlowId); !ok { + tryFindFlowID := func(q oplog.Query) (int64, error) { + var flowOp *v1.Operation + if err := h.mgr.oplog.Query(q, func(o *v1.Operation) error { + flowOp = o + return nil + }); err != nil { + return 0, fmt.Errorf("mapping remote flow ID to local ID: %w", err) + } + if flowOp != nil { + return flowOp.FlowId, nil + } + return 0, nil + } + + var err error + var flowId int64 + flowId, err = tryFindFlowID(oplog.Query{}.SetSnapshotID(op.SnapshotId)) + if err != nil { + return err + } + if flowId == 0 { + flowId, err = tryFindFlowID(oplog.Query{}. + SetOriginalFlowID(op.OriginalFlowId). + SetInstanceID(op.InstanceId)) + if err != nil { + return err + } + } + + if flowId != 0 { + op.FlowId = flowId + flowIDLru.Add(op.OriginalFlowId, flowId) + } + } + + return h.mgr.oplog.Set(op) + } + + deleteByOriginalID := func(originalID int64) error { + var foundOp *v1.Operation + if err := h.mgr.oplog.Query(oplog.Query{}.SetOriginalID(originalID), func(o *v1.Operation) error { + foundOp = o + return nil + }); err != nil { + return fmt.Errorf("mapping remote ID to local ID: %w", err) + } + + if foundOp == nil { + zap.S().Debugf("syncserver received delete for non-existent operation %v", originalID) + return nil + } + + return h.mgr.oplog.Delete(foundOp.Id) + } + + sendConfigToClient := func(config *v1.Config) error { + remoteConfig := &v1.RemoteConfig{} + var allowedRepoIDs []string + for _, repo := range config.Repos { + if slices.Contains(repo.AllowedPeerInstanceIds, clientInstanceID) { + allowedRepoIDs = append(allowedRepoIDs, repo.Id) + remoteConfig.Repos = append(remoteConfig.Repos, protoutil.RepoToRemoteRepo(repo)) + } + } + + zap.S().Debugf("syncserver determined client %v is allowlisted for repos %v", clientInstanceID, allowedRepoIDs) + + // Send the config, this is the first meaningful packet the client will receive. + // Once configuration is received, the client will start sending diffs. + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_SendConfig{ + SendConfig: &v1.SyncStreamItem_SyncActionSendConfig{ + Config: remoteConfig, + }, + }, + }); err != nil { + return fmt.Errorf("sending config to client: %w", err) + } + return nil + } + + handleSyncCommand := func(item *v1.SyncStreamItem) error { + switch action := item.Action.(type) { + case *v1.SyncStreamItem_SendConfig: + return errors.New("clients can not push configs to server") + case *v1.SyncStreamItem_DiffOperations: + diffSel := action.DiffOperations.GetHaveOperationsSelector() + + if diffSel == nil { + return connect.NewError(connect.CodeInvalidArgument, errors.New("action DiffOperations: selector is required")) + } + + // The diff selector _must_ be scoped to the instance ID of the client. + if diffSel.GetInstanceId() != clientInstanceID { + return connect.NewError(connect.CodePermissionDenied, errors.New("action DiffOperations: instance ID mismatch in diff selector")) + } + + // The diff selector _must_ specify a repo the client has access to + repo := config.FindRepoByGUID(initialConfig, diffSel.GetRepoGuid()) + if repo == nil { + zap.S().Warnf("syncserver action DiffOperations: client %q tried to diff with repo %q that does not exist", clientInstanceID, diffSel.GetRepoGuid()) + return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("action DiffOperations: repo %q not found", diffSel.GetRepoGuid())) + } + if !slices.Contains(repo.GetAllowedPeerInstanceIds(), clientInstanceID) { + zap.S().Warnf("syncserver action DiffOperations: client %q tried to diff with repo %q that they are not allowed to access", clientInstanceID, repo.Id) + return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("action DiffOperations: client is not allowed to access repo %q", repo.Id)) + } + + // These are required to be the same length for a pairwise zip. + if len(action.DiffOperations.HaveOperationIds) != len(action.DiffOperations.HaveOperationModnos) { + return connect.NewError(connect.CodeInvalidArgument, errors.New("action DiffOperations: operation IDs and modnos must be the same length")) + } + + diffSelQuery, err := protoutil.OpSelectorToQuery(diffSel) + if err != nil { + return fmt.Errorf("action DiffOperations: converting diff selector to query: %w", err) + } + + localMetadata := []oplog.OpMetadata{} + if err := h.mgr.oplog.QueryMetadata(diffSelQuery, func(metadata oplog.OpMetadata) error { + if metadata.OriginalID == 0 { + return nil // skip operations that didn't come from a remote + } + localMetadata = append(localMetadata, metadata) + return nil + }); err != nil { + return fmt.Errorf("action DiffOperations: querying local metadata: %w", err) + } + sort.Slice(localMetadata, func(i, j int) bool { + return localMetadata[i].OriginalID < localMetadata[j].OriginalID + }) + + remoteMetadata := make([]oplog.OpMetadata, len(action.DiffOperations.HaveOperationIds)) + for i, id := range action.DiffOperations.HaveOperationIds { + remoteMetadata[i] = oplog.OpMetadata{ + ID: id, + Modno: action.DiffOperations.HaveOperationModnos[i], + } + } + sort.Slice(remoteMetadata, func(i, j int) bool { + return remoteMetadata[i].ID < remoteMetadata[j].ID + }) + + requestDueToModno := 0 + requestMissingRemote := 0 + requestMissingLocal := 0 + requestIDs := []int64{} + + // This is a simple O(n) diff algorithm that compares the local and remote metadata vectors. + localIndex := 0 + remoteIndex := 0 + for localIndex < len(localMetadata) && remoteIndex < len(remoteMetadata) { + local := localMetadata[localIndex] + remote := remoteMetadata[remoteIndex] + + if local.OriginalID == remote.ID { + if local.Modno != remote.Modno { + requestIDs = append(requestIDs, local.OriginalID) + requestDueToModno++ + } + localIndex++ + remoteIndex++ + } else if local.OriginalID < remote.ID { + // the ID is found locally not remotely, request it and see if we get a delete event back + // from the client indicating that the operation was deleted. + requestIDs = append(requestIDs, local.OriginalID) + localIndex++ + requestMissingLocal++ + } else { + // the ID is found remotely not locally, request it for initial sync. + requestIDs = append(requestIDs, remote.ID) + remoteIndex++ + requestMissingRemote++ + } + } + for localIndex < len(localMetadata) { + requestIDs = append(requestIDs, localMetadata[localIndex].OriginalID) + localIndex++ + requestMissingLocal++ + } + for remoteIndex < len(remoteMetadata) { + requestIDs = append(requestIDs, remoteMetadata[remoteIndex].ID) + remoteIndex++ + requestMissingRemote++ + } + + zap.L().Debug("syncserver diff operations with client metadata", + zap.String("client_instance_id", clientInstanceID), + zap.Any("query", diffSelQuery), + zap.Int("request_due_to_modno", requestDueToModno), + zap.Int("request_local_but_not_remote", requestMissingLocal), + zap.Int("request_remote_but_not_local", requestMissingRemote), + zap.Int("request_ids_total", len(requestIDs)), + ) + if len(requestIDs) > 0 { + zap.L().Debug("syncserver sending request operations to client", zap.String("client_instance_id", clientInstanceID), zap.Any("request_ids", requestIDs)) + if err := stream.Send(&v1.SyncStreamItem{ + Action: &v1.SyncStreamItem_DiffOperations{ + DiffOperations: &v1.SyncStreamItem_SyncActionDiffOperations{ + RequestOperations: requestIDs, + }, + }, + }); err != nil { + return fmt.Errorf("sending request operations: %w", err) + } + } + + return nil + case *v1.SyncStreamItem_SendOperations: + switch event := action.SendOperations.GetEvent().Event.(type) { + case *v1.OperationEvent_CreatedOperations: + zap.L().Debug("syncserver received created operations", zap.Any("operations", event.CreatedOperations.GetOperations())) + for _, op := range event.CreatedOperations.GetOperations() { + if err := insertOrUpdate(op); err != nil { + return fmt.Errorf("action SendOperations: operation event create: %w", err) + } + } + case *v1.OperationEvent_UpdatedOperations: + zap.L().Debug("syncserver received update operations", zap.Any("operations", event.UpdatedOperations.GetOperations())) + for _, op := range event.UpdatedOperations.GetOperations() { + if err := insertOrUpdate(op); err != nil { + return fmt.Errorf("action SendOperations: operation event update: %w", err) + } + } + case *v1.OperationEvent_DeletedOperations: + zap.L().Debug("syncserver received delete operations", zap.Any("operations", event.DeletedOperations.GetValues())) + for _, id := range event.DeletedOperations.GetValues() { + if err := deleteByOriginalID(id); err != nil { + return fmt.Errorf("action SendOperations: operation event delete %d: %w", id, err) + } + } + case *v1.OperationEvent_KeepAlive: + default: + return connect.NewError(connect.CodeInvalidArgument, errors.New("action SendOperations: unknown event type")) + } + default: + return connect.NewError(connect.CodeInvalidArgument, errors.New("unknown action type")) + } + + return nil + } + + // subscribe to our own configuration for changes + configWatchCh := h.mgr.configMgr.Watch() + defer h.mgr.configMgr.StopWatching(configWatchCh) + sendConfigToClient(initialConfig) + + for { + select { + case item, ok := <-receive: + if !ok { + return nil + } + + if err := handleSyncCommand(item); err != nil { + return err + } + case sendItem, ok := <-send: // note: send channel should only be used when sending from a different goroutine than the main loop + if !ok { + return nil + } + + if err := stream.Send(sendItem); err != nil { + return err + } + case <-configWatchCh: + newConfig, err := h.mgr.configMgr.Get() + if err != nil { + zap.S().Warnf("syncserver failed to get the newest config: %v", err) + continue + } + sendConfigToClient(newConfig) + case <-ctx.Done(): + zap.S().Infof("syncserver client %q disconnected", authorizedClientPeer.InstanceId) + return ctx.Err() + } + } +} diff --git a/internal/api/syncapi/syncmanager.go b/internal/api/syncapi/syncmanager.go new file mode 100644 index 000000000..27186c8c4 --- /dev/null +++ b/internal/api/syncapi/syncmanager.go @@ -0,0 +1,136 @@ +package syncapi + +import ( + "context" + "errors" + "fmt" + "maps" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator" + "go.uber.org/zap" +) + +type SyncManager struct { + configMgr *config.ConfigManager + orchestrator *orchestrator.Orchestrator + oplog *oplog.OpLog + remoteConfigStore RemoteConfigStore + + // mutable properties + mu sync.Mutex + + syncClientRetryDelay time.Duration // the default retry delay for sync clients + + syncClients map[string]*SyncClient +} + +func NewSyncManager(configMgr *config.ConfigManager, remoteConfigStore RemoteConfigStore, oplog *oplog.OpLog, orchestrator *orchestrator.Orchestrator) *SyncManager { + return &SyncManager{ + configMgr: configMgr, + orchestrator: orchestrator, + oplog: oplog, + remoteConfigStore: remoteConfigStore, + + syncClientRetryDelay: 60 * time.Second, + syncClients: make(map[string]*SyncClient), + } +} + +// GetSyncClients returns a copy of the sync clients map. This makes the map safe to read from concurrently. +func (m *SyncManager) GetSyncClients() map[string]*SyncClient { + m.mu.Lock() + defer m.mu.Unlock() + return maps.Clone(m.syncClients) +} + +// Note: top level function will be called holding the lock, must kick off goroutines and then return. +func (m *SyncManager) RunSync(ctx context.Context) { + var syncWg sync.WaitGroup + var cancelLastSync context.CancelFunc + + configWatchCh := m.configMgr.Watch() + defer m.configMgr.StopWatching(configWatchCh) + + runSyncWithNewConfig := func() { + m.mu.Lock() + defer m.mu.Unlock() + + // TODO: rather than cancel the top level context, something clever e.g. diffing the set of peers could be done here. + if cancelLastSync != nil { + cancelLastSync() + zap.L().Info("syncmanager applying new config, waiting for existing sync goroutines to exit") + syncWg.Wait() + } + syncCtx, cancel := context.WithCancel(ctx) + cancelLastSync = cancel + + config, err := m.configMgr.Get() + if err != nil { + zap.S().Errorf("syncmanager failed to refresh config with latest changes so sync is stopped: %v", err) + return + } + + if len(config.Multihost.GetKnownHosts()) == 0 { + zap.L().Debug("syncmanager no known host peers declared, sync client exiting early") + return + } + + zap.S().Infof("syncmanager applying new config, starting sync goroutines for %d known peers", len(config.Multihost.GetKnownHosts())) + for _, knownHostPeer := range config.Multihost.KnownHosts { + if knownHostPeer.InstanceId == "" { + continue + } + + syncWg.Add(1) + go func(knownHostPeer *v1.Multihost_Peer) { + defer syncWg.Done() + zap.S().Debugf("syncmanager starting sync goroutine with peer %q", knownHostPeer.InstanceId) + err := m.runSyncWithPeerInternal(syncCtx, config, knownHostPeer) + if err != nil { + zap.S().Errorf("syncmanager error starting client for peer %q: %v", knownHostPeer.InstanceId, err) + } + }(knownHostPeer) + } + } + + runSyncWithNewConfig() + + for { + select { + case <-ctx.Done(): + return + case <-configWatchCh: + runSyncWithNewConfig() + } + } +} + +// runSyncWithPeerInternal starts the sync process with a single peer. It is expected to spawn a goroutine that will +// return when the context is canceled. Errors can only be returned upfront. +func (m *SyncManager) runSyncWithPeerInternal(ctx context.Context, config *v1.Config, knownHostPeer *v1.Multihost_Peer) error { + if config.Instance == "" { + return errors.New("local instance must set instance name before peersync can be enabled") + } + + newClient, err := NewSyncClient(m, config.Instance, knownHostPeer, m.oplog) + if err != nil { + return fmt.Errorf("creating sync client: %w", err) + } + m.mu.Lock() + m.syncClients[knownHostPeer.InstanceId] = newClient + m.mu.Unlock() + + go func() { + newClient.RunSync(ctx) + m.mu.Lock() + delete(m.syncClients, knownHostPeer.InstanceId) + m.mu.Unlock() + }() + + return nil +} diff --git a/internal/api/syncapi/uriutil.go b/internal/api/syncapi/uriutil.go new file mode 100644 index 000000000..fef31c5c3 --- /dev/null +++ b/internal/api/syncapi/uriutil.go @@ -0,0 +1,60 @@ +package syncapi + +import ( + "errors" + "net/url" +) + +var ErrNotBackrestURI = errors.New("not a backrest URI") + +func CreateRemoteRepoURI(instanceUrl string) (string, error) { + u, err := url.Parse(instanceUrl) + if err != nil { + return "", err + } + + if u.Scheme == "http" { + u.Scheme = "backrest" + } else if u.Scheme == "https" { + u.Scheme = "sbackrest" + } else { + return "", errors.New("unsupported scheme") + } + + return u.String(), nil +} + +func IsBackrestRemoteRepoURI(repoUri string) bool { + u, err := url.Parse(repoUri) + if err != nil { + return false + } + + return u.Scheme == "backrest" +} + +func InstanceForBackrestURI(repoUri string) (string, error) { + u, err := url.Parse(repoUri) + if err != nil { + return "", err + } + + if u.Scheme != "backrest" { + return "", errors.New("not a backrest URI") + } + + return u.Hostname(), nil +} + +func RepoForBackrestURI(repoUri string) (string, error) { + u, err := url.Parse(repoUri) + if err != nil { + return "", err + } + + if u.Scheme != "backrest" { + return "", errors.New("not a backrest URI") + } + + return u.Path, nil +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 000000000..c6849712a --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,129 @@ +package auth + +import ( + "encoding/base64" + "errors" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +type Authenticator struct { + config config.ConfigStore + key []byte +} + +func NewAuthenticator(key []byte, config config.ConfigStore) *Authenticator { + return &Authenticator{ + config: config, + key: key, + } +} + +var ErrUserNotFound = errors.New("user not found") +var ErrInvalidPassword = errors.New("invalid password") +var ErrInvalidKey = errors.New("invalid key") + +func (a *Authenticator) Login(username, password string) (*v1.User, error) { + config, err := a.config.Get() + if err != nil { + return nil, fmt.Errorf("get config: %w", err) + } + auth := config.GetAuth() + if auth == nil || auth.GetDisabled() { + return nil, errors.New("authentication is disabled") + } + + for _, user := range auth.GetUsers() { + if user.Name != username { + continue + } + + if err := checkPassword(user, password); err != nil { + return nil, err + } + + return user, nil + } + + return nil, ErrUserNotFound +} + +func (a *Authenticator) VerifyJWT(token string) (*v1.User, error) { + config, err := a.config.Get() + if err != nil { + return nil, fmt.Errorf("get config: %w", err) + } + auth := config.GetAuth() + if auth == nil { + return nil, fmt.Errorf("auth config not set") + } + + t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return a.key, nil + }) + + if err != nil { + return nil, fmt.Errorf("parse token: %w", err) + } + if !t.Valid { + return nil, fmt.Errorf("invalid token") + } + + subject, err := t.Claims.GetSubject() + if err != nil { + return nil, fmt.Errorf("get subject: %w", err) + } + + for _, user := range auth.GetUsers() { + if user.Name == subject { + return user, nil + } + } + + return nil, ErrUserNotFound +} + +func (a *Authenticator) CreateJWT(user *v1.User) (string, error) { + claims := &jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)), + Subject: user.Name, + } + + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + s, err := t.SignedString(a.key) + if err != nil { + return "", fmt.Errorf("sign token: %w", err) + } + + return s, nil +} + +// checkPassword returns nil if the password is correct, or an error if it is not. +func checkPassword(user *v1.User, password string) error { + switch pw := user.Password.(type) { + case *v1.User_PasswordBcrypt: + pwHash, err := base64.StdEncoding.DecodeString(pw.PasswordBcrypt) + if err != nil { + return fmt.Errorf("decode password: %w", err) + } + if err := bcrypt.CompareHashAndPassword(pwHash, []byte(password)); err != nil { + return fmt.Errorf("%w: %w", ErrInvalidPassword, err) + } + default: + return fmt.Errorf("unsupported password type: %T", pw) + } + return nil +} + +func CreatePassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", fmt.Errorf("generate password: %w", err) + } + return base64.StdEncoding.EncodeToString(hash), nil +} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 000000000..e714dada4 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,69 @@ +package auth + +import ( + "errors" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" +) + +func TestLogin(t *testing.T) { + pass := makePass(t, "testPass") + pass2 := makePass(t, "testPass2") + + config := &config.MemoryStore{ + Config: &v1.Config{ + Auth: &v1.Auth{ + Users: []*v1.User{ + { + Name: "test", + Password: &v1.User_PasswordBcrypt{ + PasswordBcrypt: pass, + }, + }, + { + Name: "anotheruser", + Password: &v1.User_PasswordBcrypt{ + PasswordBcrypt: pass2, + }, + }, + }, + }, + }, + } + + auth := NewAuthenticator([]byte("key"), config) + + tests := []struct { + name string + username string + password string + wantErr error + }{ + {"user 1 valid password", "test", "testPass", nil}, + {"user 2 valid password", "anotheruser", "testPass2", nil}, + {"user 1 wrong password", "test", "wrongPass", ErrInvalidPassword}, + {"invalid user", "nonexistent", "testPass", ErrUserNotFound}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + user, err := auth.Login(test.username, test.password) + if !errors.Is(err, test.wantErr) { + t.Fatalf("Expected error %v, got %v", test.wantErr, err) + } + if err == nil && user.Name != test.username { + t.Fatalf("Expected user name to be '%s', got '%s'", test.username, user.Name) + } + }) + } +} + +func makePass(t *testing.T, pass string) string { + p, err := CreatePassword(pass) + if err != nil { + t.Fatalf("Error creating password: %v", err) + } + return p +} diff --git a/internal/auth/bearer.go b/internal/auth/bearer.go new file mode 100644 index 000000000..55547ac80 --- /dev/null +++ b/internal/auth/bearer.go @@ -0,0 +1,13 @@ +package auth + +import ( + "fmt" + "strings" +) + +func ParseBearerToken(token string) (string, error) { + if !strings.HasPrefix(token, "Bearer ") { + return "", fmt.Errorf("invalid token") + } + return token[7:], nil +} diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 000000000..14ab07f30 --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,60 @@ +package auth + +import ( + "context" + "net/http" + + "go.uber.org/zap" +) + +type contextKey string + +func (k contextKey) String() string { + return "auth context value " + string(k) +} + +const UserContextKey contextKey = "user" +const APIKeyContextKey contextKey = "api_key" + +func RequireAuthentication(h http.Handler, auth *Authenticator) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + config, err := auth.config.Get() + if err != nil { + zap.S().Errorf("auth middleware failed to get config: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if config.GetAuth() == nil || config.GetAuth().GetDisabled() { + h.ServeHTTP(w, r) + return + } + + username, password, usesBasicAuth := r.BasicAuth() + if usesBasicAuth { + user, err := auth.Login(username, password) + if err == nil { + ctx := context.WithValue(r.Context(), UserContextKey, user) + h.ServeHTTP(w, r.WithContext(ctx)) + return + } + } + + // TODO: process the API Key + + token, err := ParseBearerToken(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, "Unauthorized (No Authorization Header)", http.StatusUnauthorized) + return + } + + user, err := auth.VerifyJWT(token) + if err != nil { + zap.S().Warnf("auth middleware blocked bad JWT: %v", err) + http.Error(w, "Unauthorized (Bad Token)", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), UserContextKey, user) + h.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index feec45102..d09d9d8c8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,18 +1,75 @@ package config import ( - "flag" + "errors" "fmt" - "os" - "path" + "slices" + "sync" - v1 "github.com/garethgeorge/resticui/gen/go/v1" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config/migrations" + "go.uber.org/zap" ) -var configDirFlag = flag.String("config_dir", "", "The directory to store the config file") +var ErrConfigNotFound = fmt.Errorf("config not found") -var Default ConfigStore = &YamlFileStore{ - Path: path.Join(configDir(*configDirFlag), "config.yaml"), +type ConfigManager struct { + Store ConfigStore + + callbacksMu sync.Mutex + changeNotifyCh []chan struct{} +} + +var _ ConfigStore = &ConfigManager{} + +func (m *ConfigManager) Get() (*v1.Config, error) { + return m.Store.Get() +} + +func (m *ConfigManager) Update(config *v1.Config) error { + err := m.Store.Update(config) + if err != nil { + return err + } + + m.callbacksMu.Lock() + changeNotifyCh := slices.Clone(m.changeNotifyCh) + m.callbacksMu.Unlock() + + for _, ch := range changeNotifyCh { + select { + case ch <- struct{}{}: + default: + } + } + + return nil +} + +func (m *ConfigManager) Watch() <-chan struct{} { + m.callbacksMu.Lock() + ch := make(chan struct{}, 1) + m.changeNotifyCh = append(m.changeNotifyCh, ch) + m.callbacksMu.Unlock() + return ch +} + +func (m *ConfigManager) StopWatching(ch <-chan struct{}) bool { + m.callbacksMu.Lock() + origLen := len(m.changeNotifyCh) + + for i := range m.changeNotifyCh { + if m.changeNotifyCh[i] != ch { + continue + } + close(m.changeNotifyCh[i]) + m.changeNotifyCh[i] = m.changeNotifyCh[len(m.changeNotifyCh)-1] + m.changeNotifyCh = m.changeNotifyCh[:len(m.changeNotifyCh)-1] + break + } + + defer m.callbacksMu.Unlock() + return len(m.changeNotifyCh) != origLen } type ConfigStore interface { @@ -22,25 +79,74 @@ type ConfigStore interface { func NewDefaultConfig() *v1.Config { return &v1.Config{ - LogDir: "/var/log/resticui", - Repos: []*v1.Repo{}, - Plans: []*v1.Plan{}, + Version: migrations.CurrentVersion, + Instance: "", + Repos: []*v1.Repo{}, + Plans: []*v1.Plan{}, + Auth: &v1.Auth{ + Disabled: true, + }, } } -func configDir(override string) string { - if override != "" { - return override - } +// TODO: merge caching validating store functions into config manager +type CachingValidatingStore struct { + ConfigStore + mu sync.Mutex + config *v1.Config +} - if env := os.Getenv("XDG_CONFIG_HOME"); env != "" { - return path.Join(env, "resticui") +func (c *CachingValidatingStore) Get() (*v1.Config, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.config != nil { + return c.config, nil } - home, err := os.UserHomeDir() + config, err := c.ConfigStore.Get() if err != nil { - panic(err) + if errors.Is(err, ErrConfigNotFound) { + c.config = NewDefaultConfig() + return c.config, nil + } + return c.config, err + } + + // Check if we need to migrate + if config.Version < migrations.CurrentVersion { + zap.S().Infof("migrating config from version %d to %d", config.Version, migrations.CurrentVersion) + if err := migrations.ApplyMigrations(config); err != nil { + return nil, err + } + + // Write back the migrated config. + if err := c.ConfigStore.Update(config); err != nil { + return nil, err + } + } + + // Validate the config + if err := ValidateConfig(config); err != nil { + return nil, err } - return fmt.Sprintf("%v/.config/resticui", home) -} \ No newline at end of file + c.config = config + return config, nil +} + +func (c *CachingValidatingStore) Update(config *v1.Config) error { + c.mu.Lock() + defer c.mu.Unlock() + + if err := ValidateConfig(config); err != nil { + return err + } + + if err := c.ConfigStore.Update(config); err != nil { + return err + } + + c.config = config + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 000000000..4a560532e --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,168 @@ +package config + +import ( + "strings" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config/migrations" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "google.golang.org/protobuf/proto" +) + +func TestMigrationsOnDefaultConfig(t *testing.T) { + config := NewDefaultConfig() + t.Logf("config: %v", config) + err := migrations.ApplyMigrations(config) + if err != nil { + t.Errorf("ApplyMigrations() error = %v, want nil", err) + } + + if config.Version != migrations.CurrentVersion { + t.Errorf("ApplyMigrations() config.Version = %v, want %v", config.Version, migrations.CurrentVersion) + } +} + +func TestConfig(t *testing.T) { + dir := t.TempDir() + + testRepo := &v1.Repo{ + Id: "test-repo", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + Uri: "/tmp/test", + Password: "test", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 14, + }, + }, + }, + } + + testPlan := &v1.Plan{ + Id: "test-plan", + Repo: "test-repo", + Paths: []string{"/tmp/foo"}, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", + }, + }, + } + + tests := []struct { + name string + config *v1.Config + wantErr bool + wantErrContains string + store ConfigStore + }{ + { + name: "default config", + config: NewDefaultConfig(), + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/default-config.json"}}, + }, + { + name: "simple valid config", + config: &v1.Config{ + Repos: []*v1.Repo{testRepo}, + Plans: []*v1.Plan{testPlan}, + }, + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/valid-config.json"}}, + }, + { + name: "plan references non-existent repo", + config: &v1.Config{ + Plans: []*v1.Plan{testPlan}, + }, + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/invalid-config.json"}}, + wantErr: true, + wantErrContains: "repo \"test-repo\" not found", + }, + { + name: "repo with duplicate id", + config: &v1.Config{ + Repos: []*v1.Repo{ + testRepo, + testRepo, + }, + }, + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/invalid-config2.json"}}, + wantErr: true, + wantErrContains: "repo test-repo: duplicate id", + }, + { + name: "plan with bad cron", + config: &v1.Config{ + Repos: []*v1.Repo{ + testRepo, + }, + Plans: []*v1.Plan{ + { + Id: "test-plan", + Repo: "test-repo", + Paths: []string{"/tmp/foo"}, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "bad cron", + }, + }, + }, + }, + }, + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/invalid-config3.json"}}, + wantErr: true, + wantErrContains: "invalid cron \"bad cron\"", + }, + { + name: "plan with bad interval days", + config: &v1.Config{ + Repos: []*v1.Repo{ + testRepo, + }, + Plans: []*v1.Plan{ + { + Id: "test-plan", + Repo: "test-repo", + Paths: []string{"/tmp/foo"}, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 0, + }, + }, + }, + }, + }, + store: &CachingValidatingStore{ConfigStore: &JsonFileStore{Path: dir + "/invalid-config3.json"}}, + wantErr: true, + wantErrContains: "invalid max frequency days", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.store.Update(tc.config) + if (err != nil) != tc.wantErr { + t.Errorf("Config.Update() error = %v, wantErr %v", err, tc.wantErr) + } + + if tc.wantErrContains != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErrContains)) { + t.Errorf("Config.Update() error = %v, wantErrContains %v", err, tc.wantErrContains) + } + + if err == nil { + config, err := tc.store.Get() + if err != nil { + t.Errorf("Config.Get() error = %v, wantErr nil", err) + } + + if !proto.Equal(config, tc.config) { + t.Errorf("Config.Get() = %v, want %v", config, tc.config) + } + } + }) + } +} diff --git a/internal/config/configutil.go b/internal/config/configutil.go new file mode 100644 index 000000000..654cab217 --- /dev/null +++ b/internal/config/configutil.go @@ -0,0 +1,32 @@ +package config + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func FindPlan(cfg *v1.Config, planID string) *v1.Plan { + for _, plan := range cfg.Plans { + if plan.Id == planID { + return plan + } + } + return nil +} + +func FindRepo(cfg *v1.Config, repoID string) *v1.Repo { + for _, repo := range cfg.Repos { + if repo.Id == repoID { + return repo + } + } + return nil +} + +func FindRepoByGUID(cfg *v1.Config, guid string) *v1.Repo { + for _, repo := range cfg.Repos { + if repo.Guid == guid { + return repo + } + } + return nil +} diff --git a/internal/config/jsonstore.go b/internal/config/jsonstore.go new file mode 100644 index 000000000..2cbfe6ef9 --- /dev/null +++ b/internal/config/jsonstore.go @@ -0,0 +1,115 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/natefinch/atomic" + "google.golang.org/protobuf/encoding/protojson" +) + +var ( + configKeepVersions = 10 +) + +type JsonFileStore struct { + Path string + mu sync.Mutex +} + +var _ ConfigStore = &JsonFileStore{} + +func (f *JsonFileStore) Get() (*v1.Config, error) { + f.mu.Lock() + defer f.mu.Unlock() + + data, err := os.ReadFile(f.Path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, ErrConfigNotFound + } + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config v1.Config + if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return &config, nil +} + +func (f *JsonFileStore) Update(config *v1.Config) error { + f.mu.Lock() + defer f.mu.Unlock() + + data, err := protojson.MarshalOptions{ + Indent: " ", + Multiline: true, + }.Marshal(config) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + + err = os.MkdirAll(filepath.Dir(f.Path), 0755) + if err != nil { + return fmt.Errorf("create config directory: %w", err) + } + + // backup the old config file + if err := f.makeBackup(); err != nil { + return fmt.Errorf("backup config file: %w", err) + } + + err = atomic.WriteFile(f.Path, bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("write config file: %w", err) + } + + // only the user running backrest should be able to read the config. + if err := os.Chmod(f.Path, 0600); err != nil { + return fmt.Errorf("chmod(0600) config file: %w", err) + } + + return nil +} + +func (f *JsonFileStore) makeBackup() error { + curConfig, err := os.ReadFile(f.Path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + + // backup the current config file + backupName := fmt.Sprintf("%s.bak.%s", f.Path, time.Now().Format("2006-01-02-15-04-05")) + if err := atomic.WriteFile(backupName, bytes.NewBuffer(curConfig)); err != nil { + return err + } + if err := os.Chmod(backupName, 0600); err != nil { + return err + } + + // only keep the last 10 versions + files, err := filepath.Glob(f.Path + ".bak.*") + if err != nil { + return err + } + if len(files) > configKeepVersions { + for _, file := range files[:len(files)-configKeepVersions] { + if err := os.Remove(file); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/config/memstore.go b/internal/config/memstore.go new file mode 100644 index 000000000..3d03b008a --- /dev/null +++ b/internal/config/memstore.go @@ -0,0 +1,27 @@ +package config + +import ( + "sync" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +type MemoryStore struct { + mu sync.Mutex + Config *v1.Config +} + +var _ ConfigStore = &MemoryStore{} + +func (c *MemoryStore) Get() (*v1.Config, error) { + c.mu.Lock() + defer c.mu.Unlock() + return c.Config, nil +} + +func (c *MemoryStore) Update(config *v1.Config) error { + c.mu.Lock() + defer c.mu.Unlock() + c.Config = config + return nil +} diff --git a/internal/config/migrations/003relativescheduling.go b/internal/config/migrations/003relativescheduling.go new file mode 100644 index 000000000..574ea0188 --- /dev/null +++ b/internal/config/migrations/003relativescheduling.go @@ -0,0 +1,25 @@ +package migrations + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" +) + +var migration003RelativeScheduling = func(config *v1.Config) { + zap.L().Info("applying config migration 003: relative scheduling") + // loop over plans and examine prune policy's + for _, repo := range config.Repos { + prunePolicy := repo.GetPrunePolicy() + if prunePolicy == nil { + continue + } + + if schedule := repo.GetPrunePolicy().GetSchedule(); schedule != nil { + schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME + } + + if schedule := repo.GetCheckPolicy().GetSchedule(); schedule != nil { + schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME + } + } +} diff --git a/internal/config/migrations/003relativescheduling_test.go b/internal/config/migrations/003relativescheduling_test.go new file mode 100644 index 000000000..13bf4baa5 --- /dev/null +++ b/internal/config/migrations/003relativescheduling_test.go @@ -0,0 +1,42 @@ +package migrations + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "google.golang.org/protobuf/proto" +) + +func Test003Migration(t *testing.T) { + config := &v1.Config{ + Repos: []*v1.Repo{ + { + Id: "prune", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + }, + }, + }, + }, + } + + want := proto.Clone(config).(*v1.Config) + want.Repos[0].PrunePolicy.Schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME + want.Repos[0].CheckPolicy.Schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME + + migration003RelativeScheduling(config) + + if !proto.Equal(config, want) { + t.Errorf("got %v, want %v", config, want) + } +} diff --git a/internal/config/migrations/004repoguid.go b/internal/config/migrations/004repoguid.go new file mode 100644 index 000000000..eb9bc92d8 --- /dev/null +++ b/internal/config/migrations/004repoguid.go @@ -0,0 +1,15 @@ +package migrations + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/cryptoutil" +) + +var migration004RepoGuid = func(config *v1.Config) { + for _, repo := range config.Repos { + if repo.Guid != "" { + continue + } + repo.Guid = cryptoutil.MustRandomID(cryptoutil.DefaultIDBits) + } +} diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go new file mode 100644 index 000000000..2b4223956 --- /dev/null +++ b/internal/config/migrations/migrations.go @@ -0,0 +1,50 @@ +package migrations + +import ( + "fmt" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +var migrations = []*func(*v1.Config){ + &noop, // migration001PrunePolicy is deprecated + &noop, // migration002Schedules is deprecated + &migration003RelativeScheduling, + &migration004RepoGuid, +} + +var CurrentVersion = int32(len(migrations)) + +func ApplyMigrations(config *v1.Config) error { + if config.Version == 0 { + if proto.Equal(config, &v1.Config{}) { + config.Version = CurrentVersion + return nil + } + return fmt.Errorf("config version 0 is invalid") + } + + startMigration := int(config.Version) + if startMigration < 0 { + startMigration = 0 + } else if startMigration > int(CurrentVersion) { + zap.S().Warnf("config version %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("config version %d is greater than the latest known config format %d", startMigration, CurrentVersion) + } + + for idx := startMigration; idx < len(migrations); idx += 1 { + m := migrations[idx] + if m == &noop { + return fmt.Errorf("config version %d is too old to migrate, please try first upgrading to backrest 1.4.0 which is the last version that may be compatible with your config", config.Version) + } + (*m)(config) + } + config.Version = CurrentVersion + return nil +} + +var noop = func(config *v1.Config) { + // do nothing +} diff --git a/internal/config/migrations/migrations_test.go b/internal/config/migrations/migrations_test.go new file mode 100644 index 000000000..ffe9dcbce --- /dev/null +++ b/internal/config/migrations/migrations_test.go @@ -0,0 +1,64 @@ +package migrations + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func TestApplyMigrations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config *v1.Config + wantErr bool + }{ + { + name: "too old to migrate", + config: &v1.Config{ + Version: 1, + }, + wantErr: true, // too old to migrate + }, + { + name: "empty config", + config: &v1.Config{}, + wantErr: false, + }, + { + name: "latest version", + config: &v1.Config{ + Version: CurrentVersion, + }, + }, + { + name: "apply relative scheduling migration", + config: &v1.Config{ + Version: 2, // higest version that still needs the migration + Repos: []*v1.Repo{ + { + Id: "repo-relative", + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := ApplyMigrations(tc.config) + if (err != nil) != tc.wantErr { + t.Errorf("ApplyMigrations() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} diff --git a/internal/config/validate.go b/internal/config/validate.go index c12eb4559..c1fc598fd 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -3,66 +3,218 @@ package config import ( "errors" "fmt" + "slices" "strings" - v1 "github.com/garethgeorge/resticui/gen/go/v1" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config/validationutil" + "github.com/garethgeorge/backrest/internal/protoutil" "github.com/hashicorp/go-multierror" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) -func validateConfig(c *v1.Config) error { - if c.LogDir == "" { - return errors.New("log_dir is required") +func ValidateConfig(c *v1.Config) error { + var err error + + if e := validationutil.ValidateID(c.Instance, validationutil.IDMaxLen); e != nil { + if errors.Is(e, validationutil.ErrEmpty) { + zap.L().Warn("ACTION REQUIRED: instance ID is empty, will be required in a future update. Please open the backrest UI to set a unique instance ID. Until fixed this warning (and related errors) will print periodically.") + } else { + err = multierror.Append(err, fmt.Errorf("instance ID %q invalid: %w", c.Instance, e)) + } + } + + if e := validateAuth(c.Auth); e != nil { + err = multierror.Append(err, fmt.Errorf("auth: %w", e)) } - var err error repos := make(map[string]*v1.Repo) if c.Repos != nil { for _, repo := range c.Repos { if e := validateRepo(repo); e != nil { - err = multierror.Append(e, fmt.Errorf("repo %s: %w", repo.GetId(), err)) + err = multierror.Append(err, fmt.Errorf("repo %s: %w", repo.GetId(), e)) + } + if _, ok := repos[repo.Id]; ok { + err = multierror.Append(err, fmt.Errorf("repo %s: duplicate id", repo.GetId())) } - repos[repo.GetId()] = repo + repos[repo.Id] = repo } + slices.SortFunc(c.Repos, func(a, b *v1.Repo) int { + if a.Id < b.Id { + return -1 + } + return 1 + }) } if c.Plans != nil { + plans := make(map[string]*v1.Plan) for _, plan := range c.Plans { - if plan.Paths == nil || len(plan.Paths) == 0 { - err = multierror.Append(err, fmt.Errorf("plan %s: path is required", plan.GetId())) + if _, ok := plans[plan.Id]; ok { + err = multierror.Append(err, fmt.Errorf("plan %s: duplicate id", plan.GetId())) } - - if plan.Repo == "" { - err = multierror.Append(err,fmt.Errorf("plan %s: repo is required", plan.GetId())) - } - - if _, ok := repos[plan.Repo]; !ok { - err = multierror.Append(err, fmt.Errorf("plan %s: repo %s not found", plan.GetId(), plan.Repo)) + plans[plan.Id] = plan + if e := validatePlan(plan, repos); e != nil { + err = multierror.Append(err, fmt.Errorf("plan %s: %w", plan.GetId(), e)) } } - } + slices.SortFunc(c.Plans, func(a, b *v1.Plan) int { + if a.Id < b.Id { + return -1 + } + return 1 + }) + } + + if e := validateMultihost(c); e != nil { + err = multierror.Append(err, fmt.Errorf("multihost: %w", e)) + } return err } func validateRepo(repo *v1.Repo) error { var err error - if repo.GetId() == "" { - err = multierror.Append(err, errors.New("id is required")) + + if e := validationutil.ValidateID(repo.Id, 0); e != nil { + err = multierror.Append(err, fmt.Errorf("id %q invalid: %w", repo.Id, e)) + } + + // GUID or AutoInitialize must be set, but not both. + if repo.Guid != "" { + if repo.AutoInitialize { + err = multierror.Append(err, fmt.Errorf("auto_initialize set with guid but guid implies that repo is already initialized")) + } + if len(repo.Guid) != 64 { // 64 bits in hex + err = multierror.Append(err, fmt.Errorf("guid %q invalid: must be 64 characters", repo.Guid)) + } + } else if !repo.AutoInitialize { + err = multierror.Append(err, fmt.Errorf("guid is required unless using auto_initialize to implicitly initialize repos")) } - if repo.GetUri() == "" { + if repo.Uri == "" { err = multierror.Append(err, errors.New("uri is required")) } - if repo.GetPassword() == "" { - err = multierror.Append(err, errors.New("password is required")) + if repo.PrunePolicy.GetSchedule() != nil { + if e := protoutil.ValidateSchedule(repo.PrunePolicy.GetSchedule()); e != nil { + err = multierror.Append(err, fmt.Errorf("prune policy schedule: %w", e)) + } + } + + if repo.CheckPolicy.GetSchedule() != nil { + if e := protoutil.ValidateSchedule(repo.CheckPolicy.GetSchedule()); e != nil { + err = multierror.Append(err, fmt.Errorf("check policy schedule: %w", e)) + } } - for _, env := range repo.GetEnv() { + for _, env := range repo.Env { if !strings.Contains(env, "=") { err = multierror.Append(err, fmt.Errorf("invalid env var %s, must take format KEY=VALUE", env)) } } + slices.Sort(repo.Env) + return err -} \ No newline at end of file +} + +func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error { + var err error + if e := validationutil.ValidateID(plan.Id, 0); e != nil { + err = multierror.Append(err, fmt.Errorf("id %q invalid: %w", plan.Id, e)) + } + + if plan.Schedule != nil { + if e := protoutil.ValidateSchedule(plan.Schedule); e != nil { + err = multierror.Append(err, fmt.Errorf("backup schedule: %w", e)) + } + } + + for idx, p := range plan.Paths { + if p == "" { + err = multierror.Append(err, fmt.Errorf("path[%d] cannot be empty", idx)) + } + } + + if plan.Repo == "" { + err = multierror.Append(err, fmt.Errorf("repo is required")) + } + + if _, ok := repos[plan.Repo]; !ok { + err = multierror.Append(err, fmt.Errorf("repo %q not found", plan.Repo)) + } + + if plan.Retention != nil && plan.Retention.Policy == nil { + err = multierror.Append(err, errors.New("retention policy must be nil or must specify a policy")) + } else if policyTimeBucketed, ok := plan.Retention.GetPolicy().(*v1.RetentionPolicy_PolicyTimeBucketed); ok { + if proto.Equal(policyTimeBucketed.PolicyTimeBucketed, &v1.RetentionPolicy_TimeBucketedCounts{}) { + err = multierror.Append(err, errors.New("time bucketed policy must specify a non-empty bucket")) + } + } + + slices.Sort(plan.Paths) + + return err +} + +func validateAuth(auth *v1.Auth) error { + if auth == nil || auth.Disabled { + return nil + } + + if len(auth.Users) == 0 { + return errors.New("auth enabled but no users") + } + + for _, user := range auth.Users { + if e := validationutil.ValidateID(user.Name, 0); e != nil { + return fmt.Errorf("user %q: %w", user.Name, e) + } + if user.GetPasswordBcrypt() == "" { + return fmt.Errorf("user %q: password is required", user.Name) + } + } + + return nil +} + +func validateMultihost(config *v1.Config) (err error) { + multihost := config.GetMultihost() + if multihost == nil { + return + } + + for _, peer := range multihost.GetAuthorizedClients() { + if e := validatePeer(peer, false); e != nil { + err = multierror.Append(err, fmt.Errorf("authorized client %q: %w", peer.GetInstanceId(), e)) + } + } + + for _, peer := range multihost.GetKnownHosts() { + if e := validatePeer(peer, true); e != nil { + err = multierror.Append(err, fmt.Errorf("known host %q: %w", peer.GetInstanceId(), e)) + } + } + + return +} + +func validatePeer(peer *v1.Multihost_Peer, isKnownHost bool) error { + if e := validationutil.ValidateID(peer.InstanceId, validationutil.IDMaxLen); e != nil { + return fmt.Errorf("id %q invalid: %w", peer.InstanceId, e) + } + + if isKnownHost { + if peer.InstanceUrl == "" { + return errors.New("instance URL is required for known hosts") + } + } + + if peer.PublicKeyVerified && peer.GetPublicKey() == nil { + return errors.New("public key cannot be marked as verified if it is unset") + } + + return nil +} diff --git a/internal/config/validationutil/validationutil.go b/internal/config/validationutil/validationutil.go new file mode 100644 index 000000000..f5ffdf61f --- /dev/null +++ b/internal/config/validationutil/validationutil.go @@ -0,0 +1,43 @@ +package validationutil + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +var ( + IDMaxLen = 50 // maximum length of an ID + sanitizeIDRegex = regexp.MustCompile(`[^a-zA-Z0-9_\-\.]+`) // matches invalid characters in an ID + idRegex = regexp.MustCompile(`[a-zA-Z0-9_\-\.]*`) // matches a valid ID (including empty string) +) + +var ( + ErrEmpty = errors.New("empty") + ErrTooLong = errors.New("too long") + ErrInvalidChars = errors.New("contains invalid characters") +) + +func SanitizeID(id string) string { + return sanitizeIDRegex.ReplaceAllString(id, "_") +} + +// ValidateID checks if an ID is valid. +// It returns an error if the ID contains invalid characters, is empty, or is too long. +// The maxLen parameter is the maximum length of the ID. If maxLen is 0, the ID length is not checked. +func ValidateID(id string, maxLen int) error { + if strings.HasPrefix(id, "_") && strings.HasSuffix(id, "_") { + return errors.New("IDs starting and ending with '_' are reserved by backrest") + } + if !idRegex.MatchString(id) { + return ErrInvalidChars + } + if len(id) == 0 { + return ErrEmpty + } + if maxLen > 0 && len(id) > maxLen { + return fmt.Errorf("(> %d chars): %w", maxLen, ErrTooLong) + } + return nil +} diff --git a/internal/config/validationutil/validationutil_test.go b/internal/config/validationutil/validationutil_test.go new file mode 100644 index 000000000..d505d2c47 --- /dev/null +++ b/internal/config/validationutil/validationutil_test.go @@ -0,0 +1,49 @@ +package validationutil + +import ( + "testing" +) + +func TestSanitizeID(t *testing.T) { + tcs := []struct { + name string + id string + want string + }{ + { + name: "empty", + id: "", + want: "", + }, + { + name: "no change", + id: "abc123", + want: "abc123", + }, + { + name: "spaces", + id: "a b c 1 2 3", + want: "a_b_c_1_2_3", + }, + { + name: "special characters", + id: "a!b@c#1$2%3", + want: "a_b_c_1_2_3", + }, + { + name: "unicode", + id: "a👍b👍c👍1👍2👍3", + want: "a_b_c_1_2_3", + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := SanitizeID(tc.id) + if got != tc.want { + t.Errorf("SanitizeID(%q) = %q, want %q", tc.id, got, tc.want) + } + }) + } +} diff --git a/internal/config/yamlstore.go b/internal/config/yamlstore.go deleted file mode 100644 index 830f36c98..000000000 --- a/internal/config/yamlstore.go +++ /dev/null @@ -1,115 +0,0 @@ -package config - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - - v1 "github.com/garethgeorge/resticui/gen/go/v1" - "github.com/google/renameio" - "google.golang.org/protobuf/encoding/protojson" - yaml "gopkg.in/yaml.v3" -) - -type YamlFileStore struct { - Path string - mu sync.Mutex - config *v1.Config -} - -var _ ConfigStore = &YamlFileStore{} - -func (f *YamlFileStore) Get() (*v1.Config, error) { - f.mu.Lock() - - if f.config != nil { - f.mu.Unlock() - return f.config, nil - } - - data, err := os.ReadFile(f.Path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - f.config = NewDefaultConfig() - f.mu.Unlock() - f.Update(f.config) - return f.config, nil - } - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - defer f.mu.Unlock() - - data, err = yamlToJson(data) - if err != nil { - return nil, fmt.Errorf("failed to parse YAML config: %w", err) - } - - var config v1.Config - - if err = protojson.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) - } - - if err := validateConfig(&config); err != nil { - return nil, fmt.Errorf("invalid config: %w", err) - } - - f.config = &config - return f.config, nil -} - -func (f *YamlFileStore) Update(config *v1.Config) error { - f.mu.Lock() - defer f.mu.Unlock() - - if err := validateConfig(config); err != nil { - return fmt.Errorf("invalid config: %w", err) - } - - data, err := protojson.Marshal(config) - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - data, err = jsonToYaml(data) - if err != nil { - return fmt.Errorf("failed to convert config to yaml: %w", err) - } - - err = os.MkdirAll(filepath.Dir(f.Path), 0755) - if err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - err = renameio.WriteFile(f.Path, data, 0644) - if err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - f.config = config - return nil -} - -func jsonToYaml(data []byte) ([]byte, error) { - var config interface{} - err := json.Unmarshal(data, &config) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) - } - - return yaml.Marshal(config) -} - -func yamlToJson(data []byte) ([]byte, error) { - var config interface{} - err := yaml.Unmarshal(data, &config) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) - } - - return json.Marshal(config) -} \ No newline at end of file diff --git a/internal/cryptoutil/idutil.go b/internal/cryptoutil/idutil.go new file mode 100644 index 000000000..9eac725f2 --- /dev/null +++ b/internal/cryptoutil/idutil.go @@ -0,0 +1,52 @@ +package cryptoutil + +import ( + "crypto/rand" + "encoding/binary" + "encoding/hex" +) + +var ( + DefaultIDBits = 256 +) + +func RandomUint64() (uint64, error) { + b := make([]byte, 8) + _, err := rand.Read(b) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint64(b), nil +} + +func MustRandomUint64() uint64 { + id, err := RandomUint64() + if err != nil { + panic(err) + } + return id +} + +func RandomID(bits int) (string, error) { + b := make([]byte, bits/8) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +func MustRandomID(bits int) string { + id, err := RandomID(bits) + if err != nil { + panic(err) + } + return id +} + +func TruncateID(id string, bits int) string { + if len(id)*4 < bits { + return id + } + return id[:bits/4] +} diff --git a/internal/env/environment.go b/internal/env/environment.go new file mode 100644 index 000000000..2acaff736 --- /dev/null +++ b/internal/env/environment.go @@ -0,0 +1,110 @@ +package env + +import ( + "flag" + "fmt" + "os" + "path" + "path/filepath" + "runtime" + "strings" +) + +var ( + EnvVarConfigPath = "BACKREST_CONFIG" // path to config file + EnvVarDataDir = "BACKREST_DATA" // path to data directory + EnvVarBindAddress = "BACKREST_PORT" // port to bind to (default 9898) + EnvVarBinPath = "BACKREST_RESTIC_COMMAND" // path to restic binary (default restic) +) + +var flagDataDir = flag.String("data-dir", "", "path to data directory, defaults to XDG_DATA_HOME/.local/backrest. Overrides BACKREST_DATA environment variable.") +var flagConfigPath = flag.String("config-file", "", "path to config file, defaults to XDG_CONFIG_HOME/backrest/config.json. Overrides BACKREST_CONFIG environment variable.") +var flagBindAddress = flag.String("bind-address", "", "address to bind to, defaults to 127.0.0.1:9898. Use :9898 to listen on all interfaces. Overrides BACKREST_PORT environment variable.") +var flagResticBinPath = flag.String("restic-cmd", "", "path to restic binary, defaults to a backrest managed version of restic. Overrides BACKREST_RESTIC_COMMAND environment variable.") + +// ConfigFilePath +// - *nix systems use $XDG_CONFIG_HOME/backrest/config.json +// - windows uses %APPDATA%/backrest/config.json +func ConfigFilePath() string { + if *flagConfigPath != "" { + return *flagConfigPath + } + if val := os.Getenv(EnvVarConfigPath); val != "" { + return val + } + return filepath.Join(getConfigDir(), "backrest", "config.json") +} + +// DataDir +// - *nix systems use $XDG_DATA_HOME/backrest +// - windows uses %APPDATA%/backrest/data +func DataDir() string { + if *flagDataDir != "" { + return *flagDataDir + } + if val := os.Getenv(EnvVarDataDir); val != "" { + return val + } + if val := os.Getenv("XDG_DATA_HOME"); val != "" { + return path.Join(val, "backrest") + } + + if runtime.GOOS == "windows" { + return filepath.Join(getConfigDir(), "backrest", "data") + } + return path.Join(getHomeDir(), ".local/share/backrest") +} + +func BindAddress() string { + if *flagBindAddress != "" { + return formatBindAddress(*flagBindAddress) + } + if val := os.Getenv(EnvVarBindAddress); val != "" { + return formatBindAddress(val) + } + return "127.0.0.1:9898" +} + +func ResticBinPath() string { + if *flagResticBinPath != "" { + return *flagResticBinPath + } + if val := os.Getenv(EnvVarBinPath); val != "" { + return val + } + return "" +} + +func LogsPath() string { + dataDir := DataDir() + return filepath.Join(dataDir, "processlogs") +} + +func getHomeDir() string { + home, err := os.UserHomeDir() + if err != nil { + panic(fmt.Errorf("couldn't determine home directory: %v", err)) + } + return home +} + +func getConfigDir() string { + if runtime.GOOS == "windows" { + cfgDir, err := os.UserConfigDir() + if err != nil { + panic(fmt.Errorf("couldn't determine config directory: %v", err)) + } + return cfgDir + } + if val := os.Getenv("XDG_CONFIG_HOME"); val != "" { + return val + } + return filepath.Join(getHomeDir(), ".config") +} + +func formatBindAddress(addr string) string { + if !strings.Contains(addr, ":") { + return ":" + addr + } + return addr +} diff --git a/internal/eventlog/eventlog.go b/internal/eventlog/eventlog.go deleted file mode 100644 index b2d7f240f..000000000 --- a/internal/eventlog/eventlog.go +++ /dev/null @@ -1,27 +0,0 @@ -package eventlog - -// LogFile interface captures admissable operations on a log file -type LogFile interface { - Log(event interface{}) error - Iterator() (LogIterator, error) - Close() error - Size() (int, error) -} - -type LogIterator interface { - Next() interface{} - Close() error -} - -type funcLogIterator struct { - nextFunc func() interface{} - closeFunc func() error -} - -func (f *funcLogIterator) Next() interface{} { - return f.nextFunc() -} - -func (f *funcLogIterator) Close() error { - return f.closeFunc() -} \ No newline at end of file diff --git a/internal/eventlog/simplefilelog.go b/internal/eventlog/simplefilelog.go deleted file mode 100644 index a695db2a7..000000000 --- a/internal/eventlog/simplefilelog.go +++ /dev/null @@ -1,158 +0,0 @@ -package eventlog - -import ( - "bufio" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "os" - "path/filepath" - "sync" -) - -// SimpleLogFile is a simple log file implementation that appends to a file. -type SimpleLogFile struct { - file string // path to the log file - handle *os.File // file handle - mu sync.Mutex -} - -func NewSimpleLogFile(file string) *SimpleLogFile { - return &SimpleLogFile{ - file: file, - } -} - -var _ LogFile = &SimpleLogFile{} - -func (s *SimpleLogFile) open() error { - if s.handle != nil { - return nil - } - - err := os.MkdirAll(filepath.Dir(s.file), 0755) - if err != nil { - return fmt.Errorf("failed to create parent dirs of log file %s: %w", s.file, err) - } - - f, err := os.OpenFile(s.file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - - if err != nil { - return fmt.Errorf("failed to open log file %s: %w", s.file, err) - } - - s.handle = f - - return nil -} - -func (s *SimpleLogFile) Log(event interface{}) error { - data, err := json.Marshal(event) - if err != nil { - return fmt.Errorf("failed to marshal event: %w", err) - } - - s.mu.Lock() - defer s.mu.Unlock() - - err = s.open() - if err != nil { - return err - } - - s.handle.Write(data) - s.handle.Write([]byte("\n")) - - return nil -} - -func (s *SimpleLogFile) Size() (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - - err := s.open() - if err != nil { - return 0, err - } - - stat, err := s.handle.Stat() - if err != nil { - return 0, fmt.Errorf("failed to stat log file %s: %w", s.file, err) - } - - return int(stat.Size()), nil -} - -func (s *SimpleLogFile) Close() error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.handle == nil { - return nil - } - - err := s.handle.Close() - if err != nil { - return fmt.Errorf("failed to close log file %s: %w", s.file, err) - } - s.handle = nil - - return nil -} - -// Iterator returns a function that can be called to iterate over the log file. -func (s *SimpleLogFile) Iterator() (LogIterator, error) { - s.mu.Lock() - f, err := os.OpenFile(s.file, os.O_RDONLY, 0644) - if err != nil { - s.mu.Unlock() - if errors.Is(err, os.ErrNotExist) { - return &funcLogIterator{ - nextFunc: func() interface{} { - return nil - }, - closeFunc: func() error { - return nil - }, - }, nil - } - return nil, fmt.Errorf("failed to open log file %s: %w", s.file, err) - } - - stat, err := f.Stat() - if err != nil { - s.mu.Unlock() - return nil, fmt.Errorf("failed to stat log file %s: %w", s.file, err) - } - size := int(stat.Size()) - s.mu.Unlock() - - reader := io.NewSectionReader(f, 0, int64(size)) - scanner := bufio.NewScanner(reader) - - nextFunc := func() interface{} { - if !scanner.Scan() { - return nil - } - - var event interface{} - err := json.Unmarshal(scanner.Bytes(), &event) - if err != nil { - log.Default().Printf("failed to unmarshal event: %v", err) - return nil - } - - return event - } - - closeFunc := func() error { - return f.Close() - } - - return &funcLogIterator{ - nextFunc: nextFunc, - closeFunc: closeFunc, - }, nil -} diff --git a/internal/hook/errors.go b/internal/hook/errors.go new file mode 100644 index 000000000..7baa0ac6e --- /dev/null +++ b/internal/hook/errors.go @@ -0,0 +1,44 @@ +package hook + +import ( + "fmt" + "time" +) + +// HookErrorCancel requests that the calling operation cancel itself. It must be handled explicitly caller. Subsequent hooks will be skipped. +type HookErrorRequestCancel struct { + Err error +} + +func (e HookErrorRequestCancel) Error() string { + return fmt.Sprintf("cancel: %v", e.Err.Error()) +} + +func (e HookErrorRequestCancel) Unwrap() error { + return e.Err +} + +// HookErrorFatal stops evaluation of subsequent hooks and will propagate to the hook flow's caller +type HookErrorFatal struct { + Err error +} + +func (e HookErrorFatal) Error() string { + return fmt.Sprintf("fatal: %v", e.Err.Error()) +} + +func (e HookErrorFatal) Unwrap() error { + return e.Err +} + +type RetryBackoffPolicy = func(attempt int) time.Duration + +// HookErrorRetry requests that the calling operation retry after a specified backoff duration +type HookErrorRetry struct { + Err error + Backoff RetryBackoffPolicy +} + +func (e HookErrorRetry) Error() string { + return fmt.Sprintf("retry: %v", e.Err.Error()) +} diff --git a/internal/hook/hook.go b/internal/hook/hook.go new file mode 100644 index 000000000..344e4306d --- /dev/null +++ b/internal/hook/hook.go @@ -0,0 +1,153 @@ +package hook + +import ( + "context" + "errors" + "fmt" + "math" + "reflect" + "slices" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + cfg "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/hook/types" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" +) + +func TasksTriggeredByEvent(config *v1.Config, repoID string, planID string, parentOp *v1.Operation, events []v1.Hook_Condition, vars interface{}) ([]tasks.Task, error) { + var taskSet []tasks.Task + + repo := cfg.FindRepo(config, repoID) + if repo == nil { + return nil, fmt.Errorf("repo %v not found", repoID) + } + plan := cfg.FindPlan(config, planID) + + for idx, hook := range repo.GetHooks() { + event := firstMatchingCondition(hook, events) + if event == v1.Hook_CONDITION_UNKNOWN { + continue + } + + name := fmt.Sprintf("repo/%v/hook/%v", repo.Id, idx) + task, err := newOneoffRunHookTask(name, config.Instance, repo, planID, parentOp, time.Now(), hook, event, vars) + if err != nil { + return nil, err + } + taskSet = append(taskSet, task) + } + + for idx, hook := range plan.GetHooks() { + event := firstMatchingCondition(hook, events) + if event == v1.Hook_CONDITION_UNKNOWN { + continue + } + + name := fmt.Sprintf("plan/%v/hook/%v", plan.Id, idx) + task, err := newOneoffRunHookTask(name, config.Instance, repo, planID, parentOp, time.Now(), hook, event, vars) + if err != nil { + return nil, err + } + taskSet = append(taskSet, task) + } + + return taskSet, nil +} + +func newOneoffRunHookTask(title, instanceID string, repo *v1.Repo, planID string, parentOp *v1.Operation, at time.Time, hook *v1.Hook, event v1.Hook_Condition, vars interface{}) (tasks.Task, error) { + h, err := types.DefaultRegistry().GetHandler(hook) + if err != nil { + return nil, fmt.Errorf("no handler for hook type %T", hook.Action) + } + + title = h.Name() + " hook " + title + + return &tasks.GenericOneoffTask{ + OneoffTask: tasks.OneoffTask{ + BaseTask: tasks.BaseTask{ + TaskType: "hook", + TaskName: fmt.Sprintf("run hook %v", title), + TaskRepo: repo, + TaskPlanID: planID, + }, + FlowID: parentOp.GetFlowId(), + RunAt: at, + ProtoOp: &v1.Operation{ + DisplayMessage: fmt.Sprintf("running %v triggered by %v", title, event.String()), + Op: &v1.Operation_OperationRunHook{ + OperationRunHook: &v1.OperationRunHook{ + Name: title, + Condition: event, + ParentOp: parentOp.GetId(), + }, + }, + }, + }, + Do: func(ctx context.Context, st tasks.ScheduledTask, taskRunner tasks.TaskRunner) error { + // TODO: this is a hack to get around the fact that vars is an interface{} . + v := reflect.ValueOf(&vars).Elem() + clone := reflect.New(v.Elem().Type()).Elem() + clone.Set(v.Elem()) // copy vars to clone + if field := v.Elem().FieldByName("Event"); field.IsValid() { + clone.FieldByName("Event").Set(reflect.ValueOf(event)) + } + + if err := h.Execute(ctx, hook, clone, taskRunner, event); err != nil { + err = applyHookErrorPolicy(hook.OnError, err) + return err + } + return nil + }, + }, nil +} + +func firstMatchingCondition(hook *v1.Hook, events []v1.Hook_Condition) v1.Hook_Condition { + for _, event := range events { + if slices.Contains(hook.Conditions, event) { + return event + } + } + return v1.Hook_CONDITION_UNKNOWN +} + +func applyHookErrorPolicy(onError v1.Hook_OnError, err error) error { + if err == nil || errors.As(err, &HookErrorFatal{}) || errors.As(err, &HookErrorRequestCancel{}) { + return err + } + + switch onError { + case v1.Hook_ON_ERROR_CANCEL: + return &HookErrorRequestCancel{Err: err} + case v1.Hook_ON_ERROR_FATAL: + return &HookErrorFatal{Err: err} + case v1.Hook_ON_ERROR_RETRY_1MINUTE: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + return 1 * time.Minute + }} + case v1.Hook_ON_ERROR_RETRY_10MINUTES: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + return 10 * time.Minute + }} + case v1.Hook_ON_ERROR_RETRY_EXPONENTIAL_BACKOFF: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + d := time.Duration(math.Pow(2, float64(attempt-1))) * 10 * time.Second + if attempt > 32 || d > 1*time.Hour { + return 1 * time.Hour + } + return d + }} + case v1.Hook_ON_ERROR_IGNORE: + return err + default: + panic(fmt.Sprintf("unknown on_error policy %v", onError)) + } +} + +// IsHaltingError returns true if the error is a fatal error or a request to cancel the operation +func IsHaltingError(err error) bool { + var fatalErr *HookErrorFatal + var cancelErr *HookErrorRequestCancel + var retryErr *HookErrorRetry + return errors.As(err, &fatalErr) || errors.As(err, &cancelErr) || errors.As(err, &retryErr) +} diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go new file mode 100644 index 000000000..b0a4ece7f --- /dev/null +++ b/internal/hook/hook_test.go @@ -0,0 +1,16 @@ +package hook + +import ( + "errors" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +// TestApplyHookErrorPolicy tests that applyHookErrorPolicy is defined for all values of Hook_OnError. +func TestApplyHookErrorPolicy(t *testing.T) { + values := v1.Hook_OnError(0).Descriptor().Values() + for i := 0; i < values.Len(); i++ { + applyHookErrorPolicy(v1.Hook_OnError(values.Get(i).Number()), errors.New("an error")) + } +} diff --git a/internal/hook/hookutil/httputil.go b/internal/hook/hookutil/httputil.go new file mode 100644 index 000000000..9d0063888 --- /dev/null +++ b/internal/hook/hookutil/httputil.go @@ -0,0 +1,25 @@ +package hookutil + +import ( + "fmt" + "io" + "net/http" +) + +func PostRequest(url string, contentType string, body io.Reader) (string, error) { + r, err := http.Post(url, contentType, body) + if err != nil { + return "", fmt.Errorf("send request %v: %w", url, err) + } + if r.StatusCode == 204 { + return "", nil + } else if r.StatusCode != 200 { + return "", fmt.Errorf("unexpected status %v: %s", r.StatusCode, r.Status) + } + defer r.Body.Close() + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + return "", fmt.Errorf("read response: %w", err) + } + return string(bodyBytes), nil +} diff --git a/internal/hook/hookutil/templateutil.go b/internal/hook/hookutil/templateutil.go new file mode 100644 index 000000000..eee8c81f1 --- /dev/null +++ b/internal/hook/hookutil/templateutil.go @@ -0,0 +1,33 @@ +package hookutil + +import ( + "bytes" + "fmt" + "strings" + "text/template" +) + +var ( + DefaultTemplate = `{{ .Summary }}` +) + +func RenderTemplate(text string, vars interface{}) (string, error) { + template, err := template.New("template").Parse(text) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + + buf := &bytes.Buffer{} + if err := template.Execute(buf, vars); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + + return buf.String(), nil +} + +func RenderTemplateOrDefault(template string, defaultTmpl string, vars interface{}) (string, error) { + if strings.Trim(template, " ") == "" { + return RenderTemplate(defaultTmpl, vars) + } + return RenderTemplate(template, vars) +} diff --git a/internal/hook/types/command.go b/internal/hook/types/command.go new file mode 100644 index 000000000..19457649e --- /dev/null +++ b/internal/hook/types/command.go @@ -0,0 +1,77 @@ +package types + +import ( + "context" + "errors" + "fmt" + "os/exec" + "reflect" + "runtime" + "strings" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/ioutil" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/google/shlex" +) + +type commandHandler struct{} + +func (commandHandler) Name() string { + return "command" +} + +func (commandHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + command, err := hookutil.RenderTemplate(h.GetActionCommand().GetCommand(), vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + writer := logging.WriterFromContext(ctx) + + // Parse out the shell to use if a #! prefix is present + shell := []string{"sh"} + if runtime.GOOS == "windows" { + shell = []string{"powershell", "-NoLogo", "-NoProfile", "-Command", "-"} + } + + if len(command) > 2 && command[0:2] == "#!" { + nextLine := strings.Index(command, "\n") + if nextLine == -1 { + nextLine = len(command) + } + shell, err = shlex.Split(strings.Trim(command[2:nextLine], " ")) + if err != nil { + return fmt.Errorf("parsing shell for command: %w", err) + } else if len(shell) == 0 { + return errors.New("must specify shell for command") + } + command = command[nextLine+1:] + } + + scriptWriter := &ioutil.LinePrefixer{W: writer, Prefix: []byte("[script] ")} + fmt.Fprintf(scriptWriter, "%v\n%v\n", shell, command) + scriptWriter.Close() + outputWriter := &ioutil.LinePrefixer{W: writer, Prefix: []byte("[output] ")} + defer outputWriter.Close() + + // Run the command in the specified shell + execCmd := exec.Command(shell[0], shell[1:]...) + execCmd.Stdin = strings.NewReader(command) + + stdout := &ioutil.SynchronizedWriter{W: outputWriter} + execCmd.Stderr = stdout + execCmd.Stdout = stdout + + return execCmd.Run() +} + +func (commandHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionCommand{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&commandHandler{}) +} diff --git a/internal/hook/types/discord.go b/internal/hook/types/discord.go new file mode 100644 index 000000000..09fbaa43d --- /dev/null +++ b/internal/hook/types/discord.go @@ -0,0 +1,55 @@ +package types + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" +) + +type discordHandler struct{} + +func (discordHandler) Name() string { + return "discord" +} + +func (discordHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + payload, err := hookutil.RenderTemplateOrDefault(h.GetActionDiscord().GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + l := runner.Logger(ctx) + l.Sugar().Infof("Sending discord message to %s", h.GetActionDiscord().GetWebhookUrl()) + l.Debug("Sending discord message", zap.String("payload", payload)) + + type Message struct { + Content string `json:"content"` + } + + request := Message{ + Content: payload, // leading newline looks better in discord. + } + + requestBytes, _ := json.Marshal(request) + body, err := hookutil.PostRequest(h.GetActionDiscord().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) + if err != nil { + return fmt.Errorf("sending discord message to %q: %w", h.GetActionDiscord().GetWebhookUrl(), err) + } + zap.S().Debug("Discord response", zap.String("body", body)) + return nil +} + +func (discordHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionDiscord{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&discordHandler{}) +} diff --git a/internal/hook/types/gotify.go b/internal/hook/types/gotify.go new file mode 100644 index 000000000..950392e3d --- /dev/null +++ b/internal/hook/types/gotify.go @@ -0,0 +1,83 @@ +package types + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "strings" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" +) + +type gotifyHandler struct{} + +func (gotifyHandler) Name() string { + return "gotify" +} + +func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + g := h.GetActionGotify() + + payload, err := hookutil.RenderTemplateOrDefault(g.GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + title, err := hookutil.RenderTemplateOrDefault(g.GetTitleTemplate(), "Backrest Event", vars) + if err != nil { + return fmt.Errorf("title template rendering: %w", err) + } + + priority := int(g.Priority) + + l := runner.Logger(ctx) + + message := struct { + Message string `json:"message"` + Title string `json:"title"` + Priority int `json:"priority"` + }{ + Title: title, + Priority: priority, + Message: payload, + } + + l.Sugar().Infof("Sending gotify message to %s", g.GetBaseUrl()) + l.Debug("Sending gotify message", zap.Any("message", message)) + + b, err := json.Marshal(message) + if err != nil { + return fmt.Errorf("json marshal: %w", err) + } + + baseUrl := strings.Trim(g.GetBaseUrl(), "/") + + postUrl := fmt.Sprintf( + "%s/message?token=%s", + baseUrl, + url.QueryEscape(g.GetToken())) + + body, err := hookutil.PostRequest(postUrl, "application/json", bytes.NewReader(b)) + + if err != nil { + return fmt.Errorf("send gotify message: %w", err) + } + + l.Sugar().Debugf("Gotify response: %s", body) + + return nil +} + +func (gotifyHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionGotify{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&gotifyHandler{}) +} diff --git a/internal/hook/types/healthchecks.go b/internal/hook/types/healthchecks.go new file mode 100644 index 000000000..985a3c0a9 --- /dev/null +++ b/internal/hook/types/healthchecks.go @@ -0,0 +1,65 @@ +package types + +import ( + "bytes" + "context" + "fmt" + "net/url" + "path" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.uber.org/zap" +) + +type healthchecksHandler struct{} + +func (healthchecksHandler) Name() string { + return "healthchecks" +} + +func (healthchecksHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + payload, err := hookutil.RenderTemplateOrDefault(cmd.GetActionHealthchecks().GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + l := runner.Logger(ctx) + l.Sugar().Infof("Sending healthchecks message to %s", cmd.GetActionHealthchecks().GetWebhookUrl()) + l.Debug("Sending healthchecks message", zap.String("payload", payload)) + baseURL := cmd.GetActionHealthchecks().GetWebhookUrl() + u, err := url.Parse(baseURL) + if err != nil { + return fmt.Errorf("parsing webhook URL: %w", err) + } + + switch { + case protoutil.IsStartCondition(event): + u.Path = path.Join(u.Path, "start") + case protoutil.IsErrorCondition(event): + u.Path = path.Join(u.Path, "fail") + case protoutil.IsLogCondition(event): + u.Path = path.Join(u.Path, "log") + } + + pingUrl := u.String() + + body, err := hookutil.PostRequest(pingUrl, "text/plain", bytes.NewBufferString(payload)) + if err != nil { + return fmt.Errorf("sending healthchecks message to %q: %w", pingUrl, err) + } + + l.Debug("Healthchecks response", zap.String("body", body)) + return nil +} + +func (healthchecksHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionHealthchecks{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&healthchecksHandler{}) +} diff --git a/internal/hook/types/registry.go b/internal/hook/types/registry.go new file mode 100644 index 000000000..5329eb6fc --- /dev/null +++ b/internal/hook/types/registry.go @@ -0,0 +1,45 @@ +package types + +import ( + "context" + "errors" + "fmt" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" +) + +var ErrHandlerNotFound = errors.New("handler not found") + +// defaultRegistry is the default handler registry. +var defaultRegistry = &HandlerRegistry{ + actionHandlers: make(map[reflect.Type]Handler), +} + +func DefaultRegistry() *HandlerRegistry { + return defaultRegistry +} + +type HandlerRegistry struct { + actionHandlers map[reflect.Type]Handler +} + +// RegisterHandler registers a handler with the default registry. +func (r *HandlerRegistry) RegisterHandler(handler Handler) { + r.actionHandlers[handler.ActionType()] = handler +} + +func (r *HandlerRegistry) GetHandler(hook *v1.Hook) (Handler, error) { + handler, ok := r.actionHandlers[reflect.TypeOf(hook.Action)] + if !ok { + return nil, fmt.Errorf("hook type %T: %w", hook.Action, ErrHandlerNotFound) + } + return handler, nil +} + +type Handler interface { + Name() string + Execute(ctx context.Context, hook *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error + ActionType() reflect.Type +} diff --git a/internal/hook/types/shoutrrr.go b/internal/hook/types/shoutrrr.go new file mode 100644 index 000000000..fb87a1f41 --- /dev/null +++ b/internal/hook/types/shoutrrr.go @@ -0,0 +1,45 @@ +package types + +import ( + "context" + "fmt" + "reflect" + + "github.com/containrrr/shoutrrr" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" +) + +type shoutrrrHandler struct{} + +func (shoutrrrHandler) Name() string { + return "shoutrrr" +} + +func (shoutrrrHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + payload, err := hookutil.RenderTemplateOrDefault(h.GetActionShoutrrr().GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + l := runner.Logger(ctx) + + l.Sugar().Infof("Sending shoutrrr message to %s", h.GetActionShoutrrr().GetShoutrrrUrl()) + l.Debug("Sending shoutrrr message", zap.String("payload", payload)) + + if err := shoutrrr.Send(h.GetActionShoutrrr().GetShoutrrrUrl(), payload); err != nil { + return fmt.Errorf("sending shoutrrr message to %q: %w", h.GetActionShoutrrr().GetShoutrrrUrl(), err) + } + + return nil +} + +func (shoutrrrHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionShoutrrr{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&shoutrrrHandler{}) +} diff --git a/internal/hook/types/slack.go b/internal/hook/types/slack.go new file mode 100644 index 000000000..f3da893db --- /dev/null +++ b/internal/hook/types/slack.go @@ -0,0 +1,57 @@ +package types + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" +) + +type slackHandler struct{} + +func (slackHandler) Name() string { + return "slack" +} + +func (slackHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + payload, err := hookutil.RenderTemplateOrDefault(cmd.GetActionSlack().GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + l := runner.Logger(ctx) + l.Sugar().Infof("Sending slack message to %s", cmd.GetActionSlack().GetWebhookUrl()) + l.Debug("Sending slack message", zap.String("payload", payload)) + + type Message struct { + Text string `json:"text"` + } + + request := Message{ + Text: "Backrest Notification\n" + payload, // leading newline looks better in discord. + } + + requestBytes, _ := json.Marshal(request) + + body, err := hookutil.PostRequest(cmd.GetActionSlack().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) + if err != nil { + return fmt.Errorf("sending slack message to %q: %w", cmd.GetActionSlack().GetWebhookUrl(), err) + } + + l.Debug("Slack response", zap.String("body", body)) + return nil +} + +func (slackHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionSlack{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&slackHandler{}) +} diff --git a/internal/ioutil/iobatching.go b/internal/ioutil/iobatching.go new file mode 100644 index 000000000..df60fd00e --- /dev/null +++ b/internal/ioutil/iobatching.go @@ -0,0 +1,15 @@ +package ioutil + +const DefaultBatchSize = 512 + +func Batchify[T any](items []T, batchSize int) [][]T { + var batches [][]T + for i := 0; i < len(items); i += batchSize { + end := i + batchSize + if end > len(items) { + end = len(items) + } + batches = append(batches, items[i:end]) + } + return batches +} diff --git a/internal/ioutil/ioutil.go b/internal/ioutil/ioutil.go new file mode 100644 index 000000000..696a98702 --- /dev/null +++ b/internal/ioutil/ioutil.go @@ -0,0 +1,103 @@ +package ioutil + +import ( + "bytes" + "io" + "sync" + "sync/atomic" +) + +// LimitWriter is a writer that limits the number of bytes written to it. +type LimitWriter struct { + W io.Writer + N int // bytes remaining that can be written + D int // bytes dropped so far +} + +func (l *LimitWriter) Write(p []byte) (rnw int, err error) { + rnw = len(p) + if l.N <= 0 { + l.D += len(p) + return 0, nil + } + if len(p) > l.N { + l.D += len(p) - l.N + p = p[:l.N] + } + _, err = l.W.Write(p) + l.N -= len(p) + return +} + +// LinePrefixer is a writer that prefixes each line written to it with a prefix. +type LinePrefixer struct { + W io.Writer + buf []byte + Prefix []byte +} + +func (l *LinePrefixer) Write(p []byte) (n int, err error) { + n = len(p) + l.buf = append(l.buf, p...) + if !bytes.Contains(p, []byte{'\n'}) { // no newlines in p, short-circuit out + return + } + bufOrig := l.buf + for { + i := bytes.IndexByte(l.buf, '\n') + if i < 0 { + break + } + if _, err := l.W.Write(l.Prefix); err != nil { + return 0, err + } + if _, err := l.W.Write(l.buf[:i+1]); err != nil { + return 0, err + } + l.buf = l.buf[i+1:] + } + l.buf = append(bufOrig[:0], l.buf...) + return +} + +func (l *LinePrefixer) Close() error { + if len(l.buf) > 0 { + if _, err := l.W.Write(l.Prefix); err != nil { + return err + } + if _, err := l.W.Write(l.buf); err != nil { + return err + } + } + return nil +} + +type SynchronizedWriter struct { + Mu sync.Mutex + W io.Writer +} + +var _ io.Writer = &SynchronizedWriter{} + +func (w *SynchronizedWriter) Write(p []byte) (n int, err error) { + w.Mu.Lock() + defer w.Mu.Unlock() + return w.W.Write(p) +} + +type SizeTrackingWriter struct { + size atomic.Uint64 + io.Writer +} + +func (w *SizeTrackingWriter) Write(p []byte) (n int, err error) { + n, err = w.Writer.Write(p) + w.size.Add(uint64(n)) + return +} + +// Size returns the number of bytes written to the writer. +// The value is fundamentally racy only consistent if synchronized with the writer or closed. +func (w *SizeTrackingWriter) Size() uint64 { + return w.size.Load() +} diff --git a/internal/logstore/logstore.go b/internal/logstore/logstore.go new file mode 100644 index 000000000..d10635924 --- /dev/null +++ b/internal/logstore/logstore.go @@ -0,0 +1,485 @@ +package logstore + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + + "github.com/hashicorp/go-multierror" + "go.uber.org/zap" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +var ( + ErrLogNotFound = fmt.Errorf("log not found") +) + +type LogStore struct { + dir string + inprogressDir string + mu shardedRWMutex + dbpool *sqlitex.Pool + + trackingMu sync.Mutex // guards refcount and subscribers + refcount map[string]int // id : refcount + subscribers map[string][]chan struct{} +} + +func NewLogStore(dir string) (*LogStore, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("create dir: %v", err) + } + + dbpath := filepath.Join(dir, "logs.sqlite") + dbpool, err := sqlitex.NewPool(dbpath, sqlitex.PoolOptions{ + PoolSize: 16, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, + }) + if err != nil { + return nil, fmt.Errorf("open sqlite pool: %v", err) + } + + ls := &LogStore{ + dir: dir, + inprogressDir: filepath.Join(dir, ".inprogress"), + mu: newShardedRWMutex(64), // 64 shards should be enough to avoid much contention + dbpool: dbpool, + subscribers: make(map[string][]chan struct{}), + refcount: make(map[string]int), + } + if err := ls.init(); err != nil { + return nil, fmt.Errorf("init log store: %v", err) + } + + return ls, nil +} + +func (ls *LogStore) init() error { + if err := os.MkdirAll(ls.inprogressDir, 0755); err != nil { + return fmt.Errorf("create inprogress dir: %v", err) + } + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteScript(conn, ` + PRAGMA auto_vacuum = 1; + PRAGMA journal_mode=WAL; + + CREATE TABLE IF NOT EXISTS logs ( + id TEXT PRIMARY KEY, + expiration_ts_unix INTEGER DEFAULT 0, -- unix timestamp of when the log will expire + owner_opid INTEGER DEFAULT 0, -- id of the operation that owns this log; will be used for cleanup. + data_fname TEXT, -- relative path to the file containing the log data + data_gz BLOB -- compressed log data as an alternative to data_fname + ); + + CREATE INDEX IF NOT EXISTS logs_data_fname_idx ON logs (data_fname); + CREATE INDEX IF NOT EXISTS logs_expiration_ts_unix_idx ON logs (expiration_ts_unix); + + CREATE TABLE IF NOT EXISTS version_info ( + version INTEGER NOT NULL + ); + + -- Create a table to store the schema version, will be used for migrations in the future + INSERT INTO version_info (version) + SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM version_info); + `, nil); err != nil { + return fmt.Errorf("execute init script: %v", err) + } + + // loop through all inprogress files and finalize them if they are in the database + files, err := os.ReadDir(ls.inprogressDir) + if err != nil { + return fmt.Errorf("read inprogress dir: %v", err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + fname := file.Name() + var id string + if err := sqlitex.ExecuteTransient(conn, "SELECT id FROM logs WHERE data_fname = ?", &sqlitex.ExecOptions{ + Args: []any{fname}, + ResultFunc: func(stmt *sqlite.Stmt) error { + id = stmt.ColumnText(0) + return nil + }, + }); err != nil { + return fmt.Errorf("select log: %v", err) + } + + if id != "" { + err := ls.finalizeLogFile(id, fname) + if err != nil { + zap.S().Warnf("sqlite log writer couldn't finalize dangling inprogress log file %v: %v", fname, err) + continue + } + } + if err := os.Remove(filepath.Join(ls.inprogressDir, fname)); err != nil { + zap.S().Warnf("sqlite log writer couldn't remove dangling inprogress log file %v: %v", fname, err) + } + } + + return nil +} + +func (ls *LogStore) Close() error { + return ls.dbpool.Close() +} + +func (ls *LogStore) Create(id string, parentOpID int64, ttl time.Duration) (io.WriteCloser, error) { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + // potentially prune any expired logs + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE expiration_ts_unix < ? AND expiration_ts_unix != 0", &sqlitex.ExecOptions{ + Args: []any{time.Now().Unix()}, + }); err != nil { + return nil, fmt.Errorf("prune expired logs: %v", err) + } + + // create a random file for the log while it's being written + randBytes := make([]byte, 16) + if _, err := rand.Read(randBytes); err != nil { + return nil, fmt.Errorf("generate random bytes: %v", err) + } + fname := hex.EncodeToString(randBytes) + ".log" + f, err := os.Create(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return nil, fmt.Errorf("create temp file: %v", err) + } + + expire_ts_unix := time.Unix(0, 0) + if ttl != 0 { + expire_ts_unix = time.Now().Add(ttl) + } + + // fmt.Printf("INSERT INTO logs (id, expiration_ts_unix, owner_opid, data_fname) VALUES (%v, %v, %v, %v)\n", id, expire_ts_unix.Unix(), parentOpID, fname) + + if err := sqlitex.ExecuteTransient(conn, "INSERT INTO logs (id, expiration_ts_unix, owner_opid, data_fname) VALUES (?, ?, ?, ?)", &sqlitex.ExecOptions{ + Args: []any{id, expire_ts_unix.Unix(), parentOpID, fname}, + }); err != nil { + return nil, fmt.Errorf("insert log: %v", err) + } + + ls.trackingMu.Lock() + ls.subscribers[id] = make([]chan struct{}, 0) + ls.refcount[id] = 1 + ls.trackingMu.Unlock() + + return &writer{ + ls: ls, + f: f, + fname: fname, + id: id, + }, nil +} + +func (ls *LogStore) Open(id string) (io.ReadCloser, error) { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + var found bool + var fname string + var dataGz []byte + if err := sqlitex.ExecuteTransient(conn, "SELECT data_fname, data_gz FROM logs WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{id}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + if !stmt.ColumnIsNull(0) { + fname = stmt.ColumnText(0) + } + if !stmt.ColumnIsNull(1) { + dataGz = make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, dataGz) + } + return nil + }, + }); err != nil { + return nil, fmt.Errorf("select log: %v", err) + } else if !found { + return nil, ErrLogNotFound + } + + if fname != "" { + f, err := os.Open(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return nil, fmt.Errorf("open data file: %v", err) + } + ls.trackingMu.Lock() + ls.refcount[id]++ + ls.trackingMu.Unlock() + + return &reader{ + ls: ls, + f: f, + id: id, + fname: fname, + closed: make(chan struct{}), + }, nil + } else if dataGz != nil { + gzr, err := gzip.NewReader(bytes.NewReader(dataGz)) + if err != nil { + return nil, fmt.Errorf("create gzip reader: %v", err) + } + + return gzr, nil + } else { + return nil, errors.New("log has no associated data. This shouldn't be possible") + } +} + +func (ls *LogStore) Delete(id string) error { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{id}, + }); err != nil { + return fmt.Errorf("delete log: %v", err) + } + + if conn.Changes() == 0 { + return ErrLogNotFound + } + return nil +} + +func (ls *LogStore) DeleteWithParent(parentOpID int64) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE owner_opid = ?", &sqlitex.ExecOptions{ + Args: []any{parentOpID}, + }); err != nil { + return fmt.Errorf("delete log: %v", err) + } + + return nil +} + +func (ls *LogStore) SelectAll(f func(id string, parentID int64)) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + return sqlitex.ExecuteTransient(conn, "SELECT id, owner_opid FROM logs ORDER BY owner_opid", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + f(stmt.ColumnText(0), stmt.ColumnInt64(1)) + return nil + }, + }) +} + +func (ls *LogStore) subscribe(id string) chan struct{} { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + + subs, ok := ls.subscribers[id] + if !ok { + return nil + } + + ch := make(chan struct{}) + ls.subscribers[id] = append(subs, ch) + return ch +} + +func (ls *LogStore) notify(id string) { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + subs, ok := ls.subscribers[id] + if !ok { + return + } + for _, ch := range subs { + close(ch) + } + ls.subscribers[id] = subs[:0] +} + +func (ls *LogStore) finalizeLogFile(id string, fname string) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + f, err := os.Open(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return err + } + defer f.Close() + + var dataGz bytes.Buffer + gzw := gzip.NewWriter(&dataGz) + if _, e := io.Copy(gzw, f); e != nil { + return fmt.Errorf("compress log: %v", e) + } + if e := gzw.Close(); e != nil { + return fmt.Errorf("close gzip writer: %v", err) + } + + if e := sqlitex.ExecuteTransient(conn, "UPDATE logs SET data_fname = NULL, data_gz = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{dataGz.Bytes(), id}, + }); e != nil { + return fmt.Errorf("update log: %v", e) + } else if conn.Changes() != 1 { + return fmt.Errorf("expected 1 row to be updated, got %d", conn.Changes()) + } + + return nil +} + +func (ls *LogStore) maybeReleaseTempFile(id, fname string) error { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + + _, ok := ls.refcount[id] + if ok { + return nil + } + return os.Remove(filepath.Join(ls.inprogressDir, fname)) +} + +type writer struct { + ls *LogStore + id string + fname string + f *os.File + onClose sync.Once +} + +var _ io.WriteCloser = (*writer)(nil) + +func (w *writer) Write(p []byte) (n int, err error) { + w.ls.mu.Lock(w.id) + defer w.ls.mu.Unlock(w.id) + n, err = w.f.Write(p) + if n != 0 { + w.ls.notify(w.id) + } + return +} + +func (w *writer) Close() error { + err := w.f.Close() + + w.onClose.Do(func() { + w.ls.mu.Lock(w.id) + defer w.ls.mu.Unlock(w.id) + defer w.ls.notify(w.id) + + if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { + err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) + } else { + w.ls.refcount[w.id]-- + } + + // manually close all subscribers and delete the subscriber entry from the map; there are no more writes coming. + w.ls.trackingMu.Lock() + if w.ls.refcount[w.id] == 0 { + delete(w.ls.refcount, w.id) + } + subs := w.ls.subscribers[w.id] + for _, ch := range subs { + close(ch) + } + delete(w.ls.subscribers, w.id) + w.ls.trackingMu.Unlock() + w.ls.maybeReleaseTempFile(w.id, w.fname) + }) + + return err +} + +type reader struct { + ls *LogStore + id string + fname string + f *os.File + onClose sync.Once + closed chan struct{} // unblocks any read calls e.g. can be used for early cancellation +} + +var _ io.ReadCloser = (*reader)(nil) + +func (r *reader) Read(p []byte) (n int, err error) { + r.ls.mu.RLock(r.id) + n, err = r.f.Read(p) + if err == io.EOF { + waiter := r.ls.subscribe(r.id) + r.ls.mu.RUnlock(r.id) + if waiter != nil { + select { + case <-waiter: + case <-r.closed: + return 0, io.EOF + } + } + r.ls.mu.RLock(r.id) + n, err = r.f.Read(p) + } + r.ls.mu.RUnlock(r.id) + + return +} + +func (r *reader) Close() error { + r.ls.mu.Lock(r.id) + defer r.ls.mu.Unlock(r.id) + + err := r.f.Close() + + r.onClose.Do(func() { + r.ls.trackingMu.Lock() + r.ls.refcount[r.id]-- + if r.ls.refcount[r.id] == 0 { + delete(r.ls.refcount, r.id) + } + r.ls.trackingMu.Unlock() + r.ls.maybeReleaseTempFile(r.id, r.fname) + close(r.closed) + }) + + return err +} diff --git a/internal/logstore/logstore_test.go b/internal/logstore/logstore_test.go new file mode 100644 index 000000000..3e82eed3b --- /dev/null +++ b/internal/logstore/logstore_test.go @@ -0,0 +1,297 @@ +package logstore + +import ( + "bytes" + "fmt" + "io" + "os" + "slices" + "sync" + "testing" + "time" +) + +func TestReadWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + if _, err := w.Write([]byte("hello, world")); err != nil { + t.Fatalf("write failed: %v", err) + } + + // assert that the file is on disk at this point + entries := getInprogressEntries(t, ls) + if len(entries) != 1 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } + + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != "hello, world" { + t.Fatalf("unexpected content: %s", data) + } + + entries = getInprogressEntries(t, ls) + if len(entries) != 0 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } +} + +func TestHugeReadWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + + data := bytes.Repeat([]byte("hello, world\n"), 1<<15) + if _, err := w.Write(data); err != nil { + t.Fatalf("write failed: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + readData := bytes.NewBuffer(nil) + if _, err := io.Copy(readData, r); err != nil { + t.Fatalf("read failed: %v", err) + } + if !bytes.Equal(readData.Bytes(), data) { + t.Fatalf("unexpected content") + } + + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } +} + +func TestReadWhileWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + data := bytes.NewBuffer(nil) + wantData := bytes.NewBuffer(nil) + + var wg sync.WaitGroup + var readn int64 + var readerr error + wg.Add(1) + go func() { + defer r.Close() + readn, readerr = io.Copy(data, r) + wg.Done() + }() + + for i := 0; i < 100; i++ { + str := fmt.Sprintf("hello, world %d\n", i) + wantData.WriteString(str) + + if _, err := w.Write([]byte(str)); err != nil { + t.Fatalf("write failed: %v", err) + } + + if i%2 == 0 { + time.Sleep(2 * time.Millisecond) + } + } + + fmt.Printf("trying to close writer from test...") + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + wg.Wait() + + // check that the asynchronous read completed successfully + if readerr != nil { + t.Fatalf("read failed: %v", readerr) + } + if readn == 0 || readn != int64(wantData.Len()) { + t.Fatalf("unexpected read length: %d", readn) + } + if !bytes.Equal(data.Bytes(), wantData.Bytes()) { + t.Fatalf("unexpected content: %s", data.Bytes()) + } + + // check that the finalized data matches expectations + var finalizedData bytes.Buffer + r2, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + if _, err := io.Copy(&finalizedData, r2); err != nil { + t.Fatalf("read failed: %v", err) + } + if !bytes.Equal(finalizedData.Bytes(), wantData.Bytes()) { + t.Fatalf("unexpected content: %s", finalizedData.Bytes()) + } + + if err := r2.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } +} + +func TestCreateMany(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + const n = 10 + for i := 0; i < n; i++ { + name := fmt.Sprintf("test%d", i) + w, err := ls.Create(name, 0, 0) + if err != nil { + t.Fatalf("create %q failed: %v", name, err) + } + if _, err := w.Write([]byte(fmt.Sprintf("hello, world %d", i))); err != nil { + t.Fatalf("write failed: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + } + + entries := getInprogressEntries(t, ls) + if len(entries) != 0 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } + + for i := 0; i < n; i++ { + name := fmt.Sprintf("test%d", i) + r, err := ls.Open(name) + if err != nil { + t.Fatalf("open %q failed: %v", name, err) + } + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != fmt.Sprintf("hello, world %d", i) { + t.Fatalf("unexpected content: %s", data) + } + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } + } +} + +func TestReopenStore(t *testing.T) { + d := t.TempDir() + { + ls, err := NewLogStore(d) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + + if _, err := w.Write([]byte("hello, world")); err != nil { + t.Fatalf("write failed: %v", err) + } + + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + // confirm that the file is on disk + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open first store failed: %v", err) + } + r.Close() + + if err := ls.Close(); err != nil { + t.Fatalf("close log store failed: %v", err) + } + + } + + { + ls, err := NewLogStore(d) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != "hello, world" { + t.Fatalf("unexpected content: %s", data) + } + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } + + if err := ls.Close(); err != nil { + t.Fatalf("close log store failed: %v", err) + } + } +} + +func getInprogressEntries(t *testing.T, ls *LogStore) []os.DirEntry { + entries, err := os.ReadDir(ls.inprogressDir) + if err != nil { + t.Fatalf("read dir failed: %v", err) + } + + entries = slices.DeleteFunc(entries, func(e os.DirEntry) bool { return e.IsDir() }) + return entries +} diff --git a/internal/logstore/shardedmutex.go b/internal/logstore/shardedmutex.go new file mode 100644 index 000000000..239e233b0 --- /dev/null +++ b/internal/logstore/shardedmutex.go @@ -0,0 +1,43 @@ +package logstore + +import ( + "hash/fnv" + "sync" +) + +type shardedRWMutex struct { + mu []sync.RWMutex +} + +func newShardedRWMutex(n int) shardedRWMutex { + mu := make([]sync.RWMutex, n) + return shardedRWMutex{ + mu: mu, + } +} + +func (sm *shardedRWMutex) Lock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].Lock() +} + +func (sm *shardedRWMutex) Unlock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].Unlock() +} + +func (sm *shardedRWMutex) RLock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].RLock() +} + +func (sm *shardedRWMutex) RUnlock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].RUnlock() +} + +func hash(s string) uint32 { + h := fnv.New32a() + h.Write([]byte(s)) + return h.Sum32() +} diff --git a/internal/logstore/tarmigrate.go b/internal/logstore/tarmigrate.go new file mode 100644 index 000000000..391baab34 --- /dev/null +++ b/internal/logstore/tarmigrate.go @@ -0,0 +1,104 @@ +package logstore + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "go.uber.org/zap" +) + +func MigrateTarLogsInDir(ls *LogStore, dir string) { + files, err := os.ReadDir(dir) + if errors.Is(err, os.ErrNotExist) { + return + } else if err != nil { + zap.L().Warn("tarlog migration failed to read directory", zap.String("dir", dir), zap.Error(err)) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if filepath.Ext(file.Name()) != ".tar" { + continue + } + + if err := MigrateTarLog(ls, filepath.Join(dir, file.Name())); err != nil { + zap.S().Warnf("failed to migrate tar log %q: %v", file.Name(), err) + } else { + if err := os.Remove(filepath.Join(dir, file.Name())); err != nil { + zap.S().Warnf("failed to remove fully migrated tar log %q: %v", file.Name(), err) + } + } + } +} + +func MigrateTarLog(ls *LogStore, logTar string) error { + baseName := filepath.Base(logTar) + + f, err := os.Open(logTar) + if err != nil { + return fmt.Errorf("failed to open tar file: %v", err) + } + + tarReader := tar.NewReader(f) + + var count int64 + var bytes int64 + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("failed to read tar header: %v", err) + } + + if header.Typeflag != tar.TypeReg { + continue + } + + w, err := ls.Create(baseName+"/"+strings.TrimSuffix(header.Name, ".gz"), 0, 14*24*time.Hour) + if err != nil { + return fmt.Errorf("failed to create log writer: %v", err) + } + + var r io.ReadCloser = io.NopCloser(tarReader) + if strings.HasSuffix(header.Name, ".gz") { + r, err = gzip.NewReader(tarReader) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %v", err) + } + } + + if n, err := io.Copy(w, r); err != nil { + return fmt.Errorf("failed to copy tar entry: %v", err) + } else { + bytes += n + count++ + } + + if err := r.Close(); err != nil { + return fmt.Errorf("failed to close tar entry reader: %v", err) + } + if err := w.Close(); err != nil { + return fmt.Errorf("failed to close log writer: %v", err) + } + } + + if err := f.Close(); err != nil { + return fmt.Errorf("failed to close tar file: %v", err) + } + + zap.L().Info("migrated tar log", zap.String("log", logTar), zap.Int64("entriesCopied", count), zap.Int64("bytesCopied", bytes)) + + return nil +} diff --git a/internal/metric/metric.go b/internal/metric/metric.go new file mode 100644 index 000000000..b1f499425 --- /dev/null +++ b/internal/metric/metric.go @@ -0,0 +1,98 @@ +package metric + +import ( + "net/http" + "slices" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + globalRegistry = initRegistry() +) + +func initRegistry() *Registry { + + commonDims := []string{"repo_id", "plan_id"} + + registry := &Registry{ + reg: prometheus.NewRegistry(), + backupBytesProcessed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "backrest_backup_bytes_processed", + Help: "The total number of bytes processed during a backup", + }, commonDims), + backupBytesAdded: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "backrest_backup_bytes_added", + Help: "The total number of bytes added during a backup", + }, commonDims), + backupFileWarnings: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "backrest_backup_file_warnings", + Help: "The total number of file warnings during a backup", + }, commonDims), + tasksDuration: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "backrest_tasks_duration_secs", + Help: "The duration of a task in seconds", + }, append(slices.Clone(commonDims), "task_type")), + tasksRun: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "backrest_tasks_run_total", + Help: "The total number of tasks run", + }, append(slices.Clone(commonDims), "task_type", "status")), + lastTaskStatus: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "backrest_last_task_status", + Help: "The status of the last task", + }, append(slices.Clone(commonDims), "task_type", "status")), + } + + registry.reg.MustRegister(registry.backupBytesProcessed) + registry.reg.MustRegister(registry.backupBytesAdded) + registry.reg.MustRegister(registry.backupFileWarnings) + registry.reg.MustRegister(registry.tasksDuration) + registry.reg.MustRegister(registry.tasksRun) + registry.reg.MustRegister(registry.lastTaskStatus) + + return registry +} + +func GetRegistry() *Registry { + return globalRegistry +} + +type Registry struct { + reg *prometheus.Registry + backupBytesProcessed *prometheus.GaugeVec + backupBytesAdded *prometheus.GaugeVec + backupFileWarnings *prometheus.GaugeVec + tasksDuration *prometheus.GaugeVec + tasksRun *prometheus.CounterVec + lastTaskStatus *prometheus.GaugeVec +} + +func (r *Registry) Handler() http.Handler { + return promhttp.HandlerFor(r.reg, promhttp.HandlerOpts{}) +} + +func (r *Registry) RecordTaskRun(repoID, planID, taskType string, duration_secs float64, status string) { + if repoID == "" { + repoID = "_unassociated_" + } + if planID == "" { + planID = "_unassociated_" + } + r.lastTaskStatus.DeletePartialMatch(prometheus.Labels{"repo_id": repoID, "plan_id": planID, "task_type": taskType}) + if status == "success" { + r.lastTaskStatus.WithLabelValues(repoID, planID, taskType, status).Set(0) + } else if status == "failed" { + r.lastTaskStatus.WithLabelValues(repoID, planID, taskType, status).Set(1) + } else { + r.lastTaskStatus.WithLabelValues(repoID, planID, taskType, status).Set(-1) + } + r.tasksRun.WithLabelValues(repoID, planID, taskType, status).Inc() + r.tasksDuration.WithLabelValues(repoID, planID, taskType).Set(duration_secs) +} + +func (r *Registry) RecordBackupSummary(repoID, planID string, bytesProcessed, bytesAdded int64, fileWarnings int64) { + r.backupBytesProcessed.WithLabelValues(repoID, planID).Set(float64(bytesProcessed)) + r.backupBytesAdded.WithLabelValues(repoID, planID).Set(float64(bytesAdded)) + r.backupFileWarnings.WithLabelValues(repoID, planID).Set(float64(fileWarnings)) +} diff --git a/internal/oplog/bboltstore/bboltstore.go b/internal/oplog/bboltstore/bboltstore.go new file mode 100644 index 000000000..83524f807 --- /dev/null +++ b/internal/oplog/bboltstore/bboltstore.go @@ -0,0 +1,458 @@ +package bboltstore + +import ( + "errors" + "fmt" + "os" + "path" + "slices" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/indexutil" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/serializationutil" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.etcd.io/bbolt" + bolt "go.etcd.io/bbolt" + "google.golang.org/protobuf/proto" +) + +type EventType int + +const ( + EventTypeUnknown = EventType(iota) + EventTypeOpCreated = EventType(iota) + EventTypeOpUpdated = EventType(iota) +) + +var ( + SystemBucket = []byte("oplog.system") // system stores metadata + OpLogBucket = []byte("oplog.log") // oplog stores existant operations. + RepoIndexBucket = []byte("oplog.repo_idx") // repo_index tracks IDs of operations affecting a given repo + PlanIndexBucket = []byte("oplog.plan_idx") // plan_index tracks IDs of operations affecting a given plan + FlowIdIndexBucket = []byte("oplog.flow_id_idx") // flow_id_index tracks IDs of operations affecting a given flow + InstanceIndexBucket = []byte("oplog.instance_idx") // instance_id_index tracks IDs of operations affecting a given instance + SnapshotIndexBucket = []byte("oplog.snapshot_idx") // snapshot_index tracks IDs of operations affecting a given snapshot +) + +// OpLog represents a log of operations performed. +// Operations are indexed by repo and plan. +type BboltStore struct { + db *bolt.DB +} + +var _ oplog.OpStore = &BboltStore{} + +func NewBboltStore(databasePath string) (*BboltStore, error) { + if err := os.MkdirAll(path.Dir(databasePath), 0700); err != nil { + return nil, fmt.Errorf("error creating database directory: %s", err) + } + + db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return nil, fmt.Errorf("error opening database: %s", err) + } + + o := &BboltStore{ + db: db, + } + + if err := db.Update(func(tx *bolt.Tx) error { + // Create the buckets if they don't exist + for _, bucket := range [][]byte{ + SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket, FlowIdIndexBucket, InstanceIndexBucket, + } { + if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { + return fmt.Errorf("creating bucket %s: %s", string(bucket), err) + } + } + + return nil + }); err != nil { + return nil, err + } + + return o, nil +} + +func (o *BboltStore) Close() error { + return o.db.Close() +} + +func (o *BboltStore) Version() (int64, error) { + var version int64 + o.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(SystemBucket) + if b == nil { + return nil + } + var err error + version, err = serializationutil.Btoi(b.Get([]byte("version"))) + return err + }) + return version, nil +} + +func (o *BboltStore) SetVersion(version int64) error { + return o.db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists(SystemBucket) + if err != nil { + return fmt.Errorf("creating system bucket: %w", err) + } + return b.Put([]byte("version"), serializationutil.Itob(version)) + }) +} + +// Add adds a generic operation to the operation log. +func (o *BboltStore) Add(ops ...*v1.Operation) error { + + return o.db.Update(func(tx *bolt.Tx) error { + for _, op := range ops { + err := o.addOperationHelper(tx, op) + if err != nil { + return err + } + } + return nil + }) +} + +func (o *BboltStore) Update(ops ...*v1.Operation) error { + for _, op := range ops { + if op.Id == 0 { + return errors.New("operation does not have an ID, OpLog.Update expects operation with an ID") + } + } + return o.db.Update(func(tx *bolt.Tx) error { + var err error + for _, op := range ops { + _, err = o.deleteOperationHelper(tx, op.Id) + if err != nil { + return fmt.Errorf("deleting existing value prior to update: %w", err) + } + if err := o.addOperationHelper(tx, op); err != nil { + return fmt.Errorf("adding updated value: %w", err) + } + } + return nil + }) +} + +func (o *BboltStore) Delete(ids ...int64) ([]*v1.Operation, error) { + removedOps := make([]*v1.Operation, 0, len(ids)) + err := o.db.Update(func(tx *bolt.Tx) error { + for _, id := range ids { + removed, err := o.deleteOperationHelper(tx, id) + if err != nil { + return fmt.Errorf("deleting operation %v: %w", id, err) + } + removedOps = append(removedOps, removed) + } + return nil + }) + return removedOps, err +} + +func (o *BboltStore) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, error) { + bytes := b.Get(serializationutil.Itob(id)) + if bytes == nil { + return nil, fmt.Errorf("opid %v: %w", id, oplog.ErrNotExist) + } + + var op v1.Operation + if err := proto.Unmarshal(bytes, &op); err != nil { + return nil, fmt.Errorf("error unmarshalling operation: %w", err) + } + + return &op, nil +} + +func (o *BboltStore) nextID(b *bolt.Bucket, unixTimeMs int64) (int64, error) { + seq, err := b.NextSequence() + if err != nil { + return 0, fmt.Errorf("next sequence: %w", err) + } + return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil +} + +func (o *BboltStore) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { + b := tx.Bucket(OpLogBucket) + if op.Id == 0 { + var err error + op.Id, err = o.nextID(b, time.Now().UnixMilli()) + if err != nil { + return fmt.Errorf("create next operation ID: %w", err) + } + } + + if op.FlowId == 0 { + op.FlowId = op.Id + } + + if err := protoutil.ValidateOperation(op); err != nil { + return fmt.Errorf("validating operation: %w", err) + } + + bytes, err := proto.Marshal(op) + if err != nil { + return fmt.Errorf("error marshalling operation: %w", err) + } + + if err := b.Put(serializationutil.Itob(op.Id), bytes); err != nil { + return fmt.Errorf("error putting operation into bucket: %w", err) + } + + // Update always universal indices + if op.RepoId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(RepoIndexBucket), []byte(op.RepoId), op.Id); err != nil { + return fmt.Errorf("error adding operation to repo index: %w", err) + } + } + + if op.PlanId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(PlanIndexBucket), []byte(op.PlanId), op.Id); err != nil { + return fmt.Errorf("error adding operation to repo index: %w", err) + } + } + + if op.SnapshotId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(SnapshotIndexBucket), []byte(op.SnapshotId), op.Id); err != nil { + return fmt.Errorf("error adding operation to snapshot index: %w", err) + } + } + + if op.FlowId != 0 { + if err := indexutil.IndexByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(op.FlowId), op.Id); err != nil { + return fmt.Errorf("error adding operation to flow index: %w", err) + } + } + + if op.InstanceId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(InstanceIndexBucket), []byte(op.InstanceId), op.Id); err != nil { + return fmt.Errorf("error adding operation to instance index: %w", err) + } + } + + return nil +} + +func (o *BboltStore) deleteOperationHelper(tx *bolt.Tx, id int64) (*v1.Operation, error) { + b := tx.Bucket(OpLogBucket) + + prevValue, err := o.getOperationHelper(b, id) + if err != nil { + return nil, fmt.Errorf("getting operation %v: %w", id, err) + } + + if prevValue.PlanId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(PlanIndexBucket), []byte(prevValue.PlanId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from plan index: %w", id, err) + } + } + + if prevValue.RepoId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(RepoIndexBucket), []byte(prevValue.RepoId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from repo index: %w", id, err) + } + } + + if prevValue.SnapshotId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(SnapshotIndexBucket), []byte(prevValue.SnapshotId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from snapshot index: %w", id, err) + } + } + + if prevValue.FlowId != 0 { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(prevValue.FlowId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from flow index: %w", id, err) + } + } + + if prevValue.InstanceId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(InstanceIndexBucket), []byte(prevValue.InstanceId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from instance index: %w", id, err) + } + } + + if err := b.Delete(serializationutil.Itob(id)); err != nil { + return nil, fmt.Errorf("deleting operation %v from bucket: %w", id, err) + } + + return prevValue, nil +} + +func (o *BboltStore) Get(id int64) (*v1.Operation, error) { + var op *v1.Operation + if err := o.db.View(func(tx *bolt.Tx) error { + var err error + op, err = o.getOperationHelper(tx.Bucket(OpLogBucket), id) + return err + }); err != nil { + return nil, err + } + return op, nil +} + +// Query represents a query to the operation log. +type Query struct { + RepoId *string + PlanId *string + SnapshotId *string + FlowId *int64 + InstanceId *string + Ids []int64 +} + +func (o *BboltStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + return o.queryHelper(q, func(tx *bbolt.Tx, op *v1.Operation) error { + return f(op) + }, true) +} + +func (o *BboltStore) QueryMetadata(q oplog.Query, f func(oplog.OpMetadata) error) error { + return errors.New("not implemented") +} + +func (o *BboltStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + return o.queryHelper(q, func(tx *bbolt.Tx, op *v1.Operation) error { + origId := op.Id + transformed, err := f(op) + if err != nil { + return err + } + if transformed == nil { + return nil + } + transformed.Modno = op.Modno + 1 + if _, err := o.deleteOperationHelper(tx, origId); err != nil { + return fmt.Errorf("deleting old operation: %w", err) + } + if err := o.addOperationHelper(tx, transformed); err != nil { + return fmt.Errorf("adding updated operation: %w", err) + } + return nil + }, false) +} + +func (o *BboltStore) queryHelper(query oplog.Query, do func(tx *bbolt.Tx, op *v1.Operation) error, isReadOnly bool) error { + helper := func(tx *bolt.Tx) error { + iterators := make([]indexutil.IndexIterator, 0, 5) + if query.PlanID != nil { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(PlanIndexBucket), []byte(*query.PlanID))) + } + if query.SnapshotID != nil { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(SnapshotIndexBucket), []byte(*query.SnapshotID))) + } + if query.FlowID != nil { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(*query.FlowID))) + } + if query.InstanceID != nil { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(InstanceIndexBucket), []byte(*query.InstanceID))) + } + + var ids []int64 + if len(iterators) == 0 && len(query.OpIDs) == 0 { + if query.Limit == 0 && query.Offset == 0 && !query.Reversed { + return o.forAll(tx, func(op *v1.Operation) error { + if query.Match(op) { + return do(tx, op) + } + return nil + }) + } else { + b := tx.Bucket(OpLogBucket) + c := b.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + if id, err := serializationutil.Btoi(k); err != nil { + continue // skip corrupt keys + } else { + ids = append(ids, id) + } + } + } + } else if len(iterators) > 0 { + ids = indexutil.CollectAll()(indexutil.NewJoinIterator(iterators...)) + } + ids = append(ids, query.OpIDs...) + + if query.Reversed { + slices.Reverse(ids) + } + if query.Offset > 0 { + if len(ids) <= query.Offset { + return nil + } + ids = ids[query.Offset:] + } + if query.Limit > 0 && len(ids) > query.Limit { + ids = ids[:query.Limit] + } + + return o.forOpsByIds(tx, ids, func(op *v1.Operation) error { + if query.Match(op) { + return do(tx, op) + } + return nil + }) + } + if isReadOnly { + return o.db.View(helper) + } else { + return o.db.Update(helper) + } +} + +func (o *BboltStore) forOpsByIds(tx *bolt.Tx, ids []int64, do func(*v1.Operation) error) error { + b := tx.Bucket(OpLogBucket) + for _, id := range ids { + op, err := o.getOperationHelper(b, id) + if err != nil { + return err + } + if err := do(op); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + return nil +} + +func (o *BboltStore) forAll(tx *bolt.Tx, do func(*v1.Operation) error) error { + b := tx.Bucket(OpLogBucket) + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var op v1.Operation + if err := proto.Unmarshal(v, &op); err != nil { + return fmt.Errorf("error unmarshalling operation: %w", err) + } + if err := do(&op); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + return nil +} + +func (o *BboltStore) ResetForTest(t *testing.T) { + if err := o.db.Update(func(tx *bolt.Tx) error { + for _, bucket := range [][]byte{ + SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket, FlowIdIndexBucket, InstanceIndexBucket, + } { + if err := tx.DeleteBucket(bucket); err != nil { + return fmt.Errorf("deleting bucket %s: %w", string(bucket), err) + } + if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { + return fmt.Errorf("creating bucket %s: %w", string(bucket), err) + } + } + return nil + }); err != nil { + t.Fatalf("error resetting database: %s", err) + } +} diff --git a/internal/oplog/bboltstore/indexutil/indexutil.go b/internal/oplog/bboltstore/indexutil/indexutil.go new file mode 100644 index 000000000..66147f904 --- /dev/null +++ b/internal/oplog/bboltstore/indexutil/indexutil.go @@ -0,0 +1,164 @@ +package indexutil + +import ( + "bytes" + + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/serializationutil" + bolt "go.etcd.io/bbolt" +) + +// IndexByteValue indexes a value and recordId tuple creating multimap from value to lists of associated recordIds. +func IndexByteValue(b *bolt.Bucket, value []byte, recordId int64) error { + key := serializationutil.BytesToKey(value) + key = append(key, serializationutil.Itob(recordId)...) + return b.Put(key, []byte{}) +} + +func IndexRemoveByteValue(b *bolt.Bucket, value []byte, recordId int64) error { + key := serializationutil.BytesToKey(value) + key = append(key, serializationutil.Itob(recordId)...) + return b.Delete(key) +} + +// IndexSearchByteValue searches the index given a value and returns an iterator over the associated recordIds. +func IndexSearchByteValue(b *bolt.Bucket, value []byte) IndexIterator { + return newSearchIterator(b, serializationutil.BytesToKey(value)) +} + +type IndexIterator interface { + Next() (int64, bool) +} + +type SeekableIndexIterator interface { + IndexIterator + Seek(int64) (int64, bool) // seek to the first recordId >= id and return it or return false. +} + +type IndexSearchIterator struct { + c *bolt.Cursor + k []byte + prefix []byte +} + +var _ SeekableIndexIterator = &IndexSearchIterator{} + +func newSearchIterator(b *bolt.Bucket, prefix []byte) IndexIterator { + c := b.Cursor() + k, _ := c.Seek(prefix) + return &IndexSearchIterator{ + c: c, + k: k, + prefix: prefix, + } +} + +func (i *IndexSearchIterator) Next() (int64, bool) { + if i.k == nil || !bytes.HasPrefix(i.k, i.prefix) { + return 0, false + } + id, err := serializationutil.Btoi(i.k[len(i.prefix):]) + if err != nil { + // this sholud never happen, if it does it indicates database corruption. + return 0, false + } + i.k, _ = i.c.Next() + return id, true +} + +func (i *IndexSearchIterator) Seek(id int64) (int64, bool) { + seekTo := []byte{} + seekTo = append(seekTo, i.prefix...) + seekTo = append(seekTo, serializationutil.Itob(id)...) + k, _ := i.c.Seek(seekTo) + if k == nil || !bytes.HasPrefix(k, i.prefix) { + return 0, false + } + id, err := serializationutil.Btoi(k[len(i.prefix):]) + if err != nil { + return 0, false + } + return id, true +} + +type JoinIterator struct { + iters []IndexIterator + seekables []SeekableIndexIterator +} + +func NewJoinIterator(iters ...IndexIterator) *JoinIterator { + seekables := make([]SeekableIndexIterator, 0, len(iters)) + for _, iter := range iters { + if seekable, ok := iter.(SeekableIndexIterator); ok { + seekables = append(seekables, seekable) + } else { + seekables = append(seekables, nil) + } + } + return &JoinIterator{ + iters: iters, + seekables: seekables, + } +} + +func (j *JoinIterator) Next() (int64, bool) { + if len(j.iters) == 0 { + return 0, false + } + + nexts := make([]int64, len(j.iters)) + for idx, iter := range j.iters { + id, ok := iter.Next() + if !ok { + return 0, false + } + nexts[idx] = id + } + + for { + var ok bool + maxIdx := 0 + allSame := true + for idx, id := range nexts { + if id > nexts[maxIdx] { + maxIdx = idx + } + if id != nexts[0] { + allSame = false + } + } + + if allSame { + return nexts[0], true + } + + for idx, id := range nexts { + if id == nexts[maxIdx] { + continue + } + + if j.seekables[idx] != nil { + nexts[idx], ok = j.seekables[idx].Seek(nexts[maxIdx]) + if !ok { + return 0, false + } + } else { + nexts[idx], ok = j.iters[idx].Next() + if !ok { + return 0, false + } + } + } + } +} + +type Collector func(IndexIterator) []int64 + +func CollectAll() Collector { + return func(iter IndexIterator) []int64 { + ids := make([]int64, 0, 100) + for id, ok := iter.Next(); ok; id, ok = iter.Next() { + ids = append(ids, id) + } + return ids + } +} diff --git a/internal/oplog/bboltstore/indexutil/indexutil_test.go b/internal/oplog/bboltstore/indexutil/indexutil_test.go new file mode 100644 index 000000000..e90958d5e --- /dev/null +++ b/internal/oplog/bboltstore/indexutil/indexutil_test.go @@ -0,0 +1,102 @@ +package indexutil + +import ( + "fmt" + "reflect" + "testing" + + "go.etcd.io/bbolt" +) + +func TestIndexing(t *testing.T) { + db, err := bbolt.Open(t.TempDir()+"/test.boltdb", 0600, nil) + if err != nil { + t.Fatalf("error opening database: %s", err) + } + defer db.Close() + + if err := db.Update(func(tx *bbolt.Tx) error { + b, err := tx.CreateBucket([]byte("test")) + if err != nil { + return fmt.Errorf("error creating bucket: %s", err) + } + for id := 0; id < 100; id += 1 { + if err := IndexByteValue(b, []byte("document"), int64(id)); err != nil { + return err + } + } + return nil + }); err != nil { + t.Fatalf("db.Update error: %v", err) + } + + if err := db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket([]byte("test")) + ids := CollectAll()(IndexSearchByteValue(b, []byte("document"))) + if len(ids) != 100 { + t.Errorf("want 100 ids, got %d", len(ids)) + } + ids = CollectAll()(IndexSearchByteValue(b, []byte("other"))) + if len(ids) != 0 { + t.Errorf("want 0 ids, got %d", len(ids)) + } + return nil + }); err != nil { + t.Fatalf("db.View error: %v", err) + } +} + +func TestIndexJoin(t *testing.T) { + // Arrange + db, err := bbolt.Open(t.TempDir()+"/test.boltdb", 0600, nil) + if err != nil { + t.Fatalf("error opening database: %s", err) + } + defer db.Close() + + if err := db.Update(func(tx *bbolt.Tx) error { + b, err := tx.CreateBucket([]byte("test")) + if err != nil { + return fmt.Errorf("error creating bucket: %s", err) + } + for id := 0; id < 150; id += 1 { + if err := IndexByteValue(b, []byte("document"), int64(id)); err != nil { + return err + } + } + + for id := 0; id < 100; id += 2 { + if err := IndexByteValue(b, []byte("other"), int64(id)); err != nil { + return err + } + } + + return nil + }); err != nil { + t.Fatalf("db.Update error: %v", err) + } + + if err := db.View(func(tx *bbolt.Tx) error { + // Act + b := tx.Bucket([]byte("test")) + ids := CollectAll()(NewJoinIterator(IndexSearchByteValue(b, []byte("document")), IndexSearchByteValue(b, []byte("other")))) + + // Assert + if len(ids) != 50 { + t.Errorf("want 50 ids, got %d", len(ids)) + } + + wantIds := []int64{} + for id := 0; id < 100; id += 2 { + wantIds = append(wantIds, int64(id)) + } + + if !reflect.DeepEqual(ids, wantIds) { + t.Errorf("want %v, got %v", wantIds, ids) + } + + return nil + }); err != nil { + t.Fatalf("db.View error: %v", err) + } +} diff --git a/internal/oplog/bboltstore/serializationutil/serializationutil.go b/internal/oplog/bboltstore/serializationutil/serializationutil.go new file mode 100644 index 000000000..397282bb4 --- /dev/null +++ b/internal/oplog/bboltstore/serializationutil/serializationutil.go @@ -0,0 +1,46 @@ +package serializationutil + +import ( + "encoding/binary" + "errors" +) + +var ErrInvalidLength = errors.New("invalid length") + +func Itob(v int64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(v)) + return b +} + +func Btoi(b []byte) (int64, error) { + if len(b) != 8 { + return 0, ErrInvalidLength + } + return int64(binary.BigEndian.Uint64(b)), nil +} + +func Stob(v string) []byte { + b := make([]byte, 0, len(v)+8) + b = append(b, Itob(int64(len(v)))...) + b = append(b, []byte(v)...) + return b +} + +func Btos(b []byte) (string, int64, error) { + if len(b) < 8 { + return "", 0, ErrInvalidLength + } + length, _ := Btoi(b[:8]) + if int64(len(b)) < 8+length { + return "", 0, ErrInvalidLength + } + return string(b[8 : 8+length]), 8 + length, nil +} + +func BytesToKey(b []byte) []byte { + key := make([]byte, 0, 8+len(b)) + key = append(key, Itob(int64(len(b)))...) + key = append(key, b...) + return key +} diff --git a/internal/oplog/bboltstore/serializationutil/serializationutil_test.go b/internal/oplog/bboltstore/serializationutil/serializationutil_test.go new file mode 100644 index 000000000..524a768ec --- /dev/null +++ b/internal/oplog/bboltstore/serializationutil/serializationutil_test.go @@ -0,0 +1,23 @@ +package serializationutil + +import "testing" + +func TestItoa(t *testing.T) { + nums := []int64{0, 1, 2, 3, 4, 1 << 32, int64(1) << 62} + for _, num := range nums { + b := Itob(num) + if v, _ := Btoi(b); v != num { + t.Errorf("itob/btoi failed for %d", num) + } + } +} + +func TestStob(t *testing.T) { + strs := []string{"", "a", "ab", "abc", "abcd", "abcde", "abcdef"} + for _, str := range strs { + b := Stob(str) + if val, _, _ := Btos(b); val != str { + t.Errorf("stob/btos failed for %s", str) + } + } +} diff --git a/internal/oplog/memstore/memstore.go b/internal/oplog/memstore/memstore.go new file mode 100644 index 000000000..3225afe7a --- /dev/null +++ b/internal/oplog/memstore/memstore.go @@ -0,0 +1,182 @@ +package memstore + +import ( + "slices" + "sync" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "google.golang.org/protobuf/proto" +) + +type MemStore struct { + mu sync.Mutex + operations map[int64]*v1.Operation + nextID int64 +} + +var _ oplog.OpStore = &MemStore{} + +func NewMemStore() *MemStore { + return &MemStore{ + operations: make(map[int64]*v1.Operation), + } +} + +func (m *MemStore) Version() (int64, error) { + return 0, nil +} + +func (m *MemStore) SetVersion(version int64) error { + return nil +} + +func (m *MemStore) idsForQuery(q oplog.Query) []int64 { + ids := make([]int64, 0, len(m.operations)) + for id := range m.operations { + ids = append(ids, id) + } + slices.SortFunc(ids, func(i, j int64) int { return int(i - j) }) + ids = slices.DeleteFunc(ids, func(id int64) bool { + op := m.operations[id] + return !q.Match(op) + }) + + if q.Offset > 0 { + if int(q.Offset) >= len(ids) { + ids = nil + } else { + ids = ids[q.Offset:] + } + } + + if q.Limit > 0 && len(ids) > q.Limit { + ids = ids[:q.Limit] + } + + if q.Reversed { + slices.Reverse(ids) + } + + return ids +} + +func (m *MemStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + m.mu.Lock() + defer m.mu.Unlock() + + ids := m.idsForQuery(q) + + for _, id := range ids { + if err := f(proto.Clone(m.operations[id]).(*v1.Operation)); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + + return nil +} + +func (m *MemStore) QueryMetadata(q oplog.Query, f func(meta oplog.OpMetadata) error) error { + for _, id := range m.idsForQuery(q) { + op := m.operations[id] + if err := f(oplog.OpMetadata{ + ID: op.Id, + Modno: op.Modno, + FlowID: op.FlowId, + OriginalID: op.OriginalId, + OriginalFlowID: op.OriginalFlowId, + }); err != nil { + return err + } + } + return nil +} + +func (m *MemStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + m.mu.Lock() + defer m.mu.Unlock() + + ids := m.idsForQuery(q) + + changes := make(map[int64]*v1.Operation) + + for _, id := range ids { + if op, err := f(proto.Clone(m.operations[id]).(*v1.Operation)); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } else if op != nil { + op.Modno = oplog.NewRandomModno(m.operations[id].Modno) + changes[id] = op + } + } + + // Apply changes after the loop to avoid modifying the map until the transaction is complete. + for id, op := range changes { + m.operations[id] = op + } + + return nil +} + +func (m *MemStore) Add(op ...*v1.Operation) error { + m.mu.Lock() + defer m.mu.Unlock() + + for _, o := range op { + m.nextID++ + o.Id = m.nextID + if o.FlowId == 0 { + o.FlowId = o.Id + } + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + } + + for _, o := range op { + m.operations[o.Id] = o + } + return nil +} + +func (m *MemStore) Get(opID int64) (*v1.Operation, error) { + m.mu.Lock() + defer m.mu.Unlock() + op, ok := m.operations[opID] + if !ok { + return nil, oplog.ErrNotExist + } + return op, nil +} + +func (m *MemStore) Delete(opID ...int64) ([]*v1.Operation, error) { + m.mu.Lock() + defer m.mu.Unlock() + ops := make([]*v1.Operation, 0, len(opID)) + for _, id := range opID { + ops = append(ops, m.operations[id]) + delete(m.operations, id) + } + return ops, nil +} + +func (m *MemStore) Update(op ...*v1.Operation) error { + m.mu.Lock() + defer m.mu.Unlock() + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + if _, ok := m.operations[o.Id]; !ok { + return oplog.ErrNotExist + } + m.operations[o.Id] = o + } + return nil +} diff --git a/internal/oplog/migrations.go b/internal/oplog/migrations.go new file mode 100644 index 000000000..0e25d3aaa --- /dev/null +++ b/internal/oplog/migrations.go @@ -0,0 +1,136 @@ +package oplog + +import ( + "fmt" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/ioutil" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +var migrations = []func(*OpLog) error{ + migration001FlowID, + migration002InstanceID, + migrationNoop, + migration002InstanceID, // re-run migration002InstanceID to fix improperly set instance IDs + migration003DeduplicateIndexedSnapshots, +} + +var CurrentVersion = int64(len(migrations)) + +func ApplyMigrations(oplog *OpLog) error { + startMigration, err := oplog.store.Version() + if err != nil { + zap.L().Error("failed to get migration version", zap.Error(err)) + return fmt.Errorf("couldn't get migration version: %w", err) + } + if startMigration < 0 { + startMigration = 0 + } else if startMigration > CurrentVersion { + zap.S().Warnf("oplog spec %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("oplog spec %d is greater than the latest known spec %d", startMigration, CurrentVersion) + } + + for idx := startMigration; idx < int64(len(migrations)); idx += 1 { + zap.L().Info("oplog applying data migration", zap.Int64("migration_no", idx)) + if err := migrations[idx](oplog); err != nil { + zap.L().Error("failed to apply data migration", zap.Int64("migration_no", idx), zap.Error(err)) + return fmt.Errorf("couldn't apply migration %d: %w", idx, err) + } + if err := oplog.store.SetVersion(idx + 1); err != nil { + zap.L().Error("failed to set migration version, database may be corrupt", zap.Int64("migration_no", idx), zap.Error(err)) + return fmt.Errorf("couldn't set migration version %d: %w", idx, err) + } + } + + return nil +} + +func transformOperations(oplog *OpLog, f func(op *v1.Operation) error) error { + oplog.store.Transform(SelectAll, func(op *v1.Operation) (*v1.Operation, error) { + copy := proto.Clone(op).(*v1.Operation) + err := f(copy) + if err != nil { + return nil, err + } + + if proto.Equal(copy, op) { + return nil, nil + } + + return copy, nil + }) + + return nil +} + +// migration001FlowID sets the flow ID for operations that are missing it. +// All operations with the same snapshot ID will have the same flow ID. +func migration001FlowID(oplog *OpLog) error { + snapshotIdToFlow := make(map[string]int64) + + return transformOperations(oplog, func(op *v1.Operation) error { + if op.FlowId != 0 { + return nil + } + + if op.SnapshotId == "" { + op.FlowId = op.Id + return nil + } + + if flowId, ok := snapshotIdToFlow[op.SnapshotId]; ok { + op.FlowId = flowId + } else { + snapshotIdToFlow[op.SnapshotId] = op.Id + op.FlowId = op.Id + } + + return nil + }) +} + +func migration002InstanceID(oplog *OpLog) error { + return transformOperations(oplog, func(op *v1.Operation) error { + if op.InstanceId != "" { + return nil + } + + op.InstanceId = "_unassociated_" + return nil + }) +} + +func migration003DeduplicateIndexedSnapshots(oplog *OpLog) error { + var snapshotIDs = make(map[string]struct{}) + var deleteIDs []int64 + if err := oplog.Query(SelectAll, func(op *v1.Operation) error { + if _, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { + if _, ok := snapshotIDs[op.SnapshotId]; ok { + deleteIDs = append(deleteIDs, op.Id) + } else { + snapshotIDs[op.SnapshotId] = struct{}{} + } + } + return nil + }); err != nil { + return err + } + + if len(deleteIDs) == 0 { + return nil + } + + for _, batch := range ioutil.Batchify(deleteIDs, ioutil.DefaultBatchSize) { + if _, err := oplog.store.Delete(batch...); err != nil { + return err + } + } + return nil +} + +// migrationNoop is a migration that does nothing; replaces deprecated migrations. +func migrationNoop(oplog *OpLog) error { + return nil +} diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go new file mode 100644 index 000000000..4b6b397c4 --- /dev/null +++ b/internal/oplog/oplog.go @@ -0,0 +1,198 @@ +package oplog + +import ( + "errors" + "slices" + "sync" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +type OperationEvent int + +const ( + OPERATION_ADDED OperationEvent = iota + OPERATION_UPDATED + OPERATION_DELETED +) + +var ( + ErrStopIteration = errors.New("stop iteration") + ErrNotExist = errors.New("operation does not exist") + ErrExist = errors.New("operation already exists") + + NullOPID = int64(0) +) + +type Subscription = func(ops []*v1.Operation, event OperationEvent) + +type subAndQuery struct { + f *Subscription + q Query +} + +type OpLog struct { + store OpStore + + subscribersMu sync.Mutex + subscribers []subAndQuery +} + +func NewOpLog(store OpStore) (*OpLog, error) { + o := &OpLog{ + store: store, + } + + if err := ApplyMigrations(o); err != nil { + return nil, err + } + + return o, nil +} + +func (o *OpLog) curSubscribers() []subAndQuery { + o.subscribersMu.Lock() + defer o.subscribersMu.Unlock() + return slices.Clone(o.subscribers) +} + +func (o *OpLog) notify(ops []*v1.Operation, event OperationEvent) { + for _, sub := range o.curSubscribers() { + notifyOps := make([]*v1.Operation, 0, len(ops)) + for _, op := range ops { + if sub.q.Match(op) { + notifyOps = append(notifyOps, op) + } + } + if len(notifyOps) > 0 { + (*sub.f)(notifyOps, event) + } + } +} + +func (o *OpLog) Query(q Query, f func(*v1.Operation) error) error { + return o.store.Query(q, f) +} + +func (o *OpLog) QueryMetadata(q Query, f func(OpMetadata) error) error { + return o.store.QueryMetadata(q, f) +} + +func (o *OpLog) Subscribe(q Query, f *Subscription) { + o.subscribersMu.Lock() + defer o.subscribersMu.Unlock() + o.subscribers = append(o.subscribers, subAndQuery{f: f, q: q}) +} + +func (o *OpLog) Unsubscribe(f *Subscription) error { + o.subscribersMu.Lock() + defer o.subscribersMu.Unlock() + for i, sub := range o.subscribers { + if sub.f == f { + o.subscribers = append(o.subscribers[:i], o.subscribers[i+1:]...) + return nil + } + } + return errors.New("subscription not found") +} + +func (o *OpLog) Get(opID int64) (*v1.Operation, error) { + return o.store.Get(opID) +} + +func (o *OpLog) Add(ops ...*v1.Operation) error { + for _, o := range ops { + if o.Id != 0 { + return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") + } + if o.Modno == 0 { + o.Modno = NewRandomModno(0) + } + } + + if err := o.store.Add(ops...); err != nil { + return err + } + + o.notify(ops, OPERATION_ADDED) + return nil +} + +func (o *OpLog) Update(ops ...*v1.Operation) error { + for _, o := range ops { + if o.Id == 0 { + return errors.New("operation does not have an ID, OpLog.Update is expected to have an ID") + } + o.Modno = NewRandomModno(o.Modno) + } + + if err := o.store.Update(ops...); err != nil { + return err + } + + o.notify(ops, OPERATION_UPDATED) + return nil +} + +// Set is an alias for Update that does not increment the modno, provided for use by the syncapi. +func (o *OpLog) Set(op *v1.Operation) error { + var err error + if op.Id == 0 { + err = o.store.Add(op) + } else { + err = o.store.Update(op) + if errors.Is(err, ErrNotExist) { + err = o.store.Add(op) + } + } + if err != nil { + return err + } + o.notify([]*v1.Operation{op}, OPERATION_UPDATED) + return nil +} + +func (o *OpLog) Delete(opID ...int64) error { + removedOps, err := o.store.Delete(opID...) + if err != nil { + return err + } + + o.notify(removedOps, OPERATION_DELETED) + return nil +} + +func (o *OpLog) Transform(q Query, f func(*v1.Operation) (*v1.Operation, error)) error { + return o.store.Transform(q, f) +} + +type OpStore interface { + // Query returns all operations that match the query. + Query(q Query, f func(*v1.Operation) error) error + // QueryMetadata is like Query, but only returns metadata about the operations. + // this is useful for very high performance scans that don't deserialize the operation itself. + QueryMetadata(q Query, f func(OpMetadata) error) error + // Get returns the operation with the given ID. + Get(opID int64) (*v1.Operation, error) + // Add adds the given operations to the store. + Add(op ...*v1.Operation) error + // Update updates the given operations in the store. + Update(op ...*v1.Operation) error + // Delete removes the operations with the given IDs from the store, and returns the removed operations. + Delete(opID ...int64) ([]*v1.Operation, error) + // Transform applies the given function to each operation that matches the query. + Transform(q Query, f func(*v1.Operation) (*v1.Operation, error)) error + // Version returns the current data version + Version() (int64, error) + // SetVersion sets the data version + SetVersion(version int64) error +} + +// OpMetadata is a struct that contains metadata about an operation without fetching the operation itself. +type OpMetadata struct { + ID int64 + FlowID int64 + Modno int64 + OriginalID int64 + OriginalFlowID int64 +} diff --git a/internal/oplog/query.go b/internal/oplog/query.go new file mode 100644 index 000000000..c3cb2ce2a --- /dev/null +++ b/internal/oplog/query.go @@ -0,0 +1,128 @@ +package oplog + +import v1 "github.com/garethgeorge/backrest/gen/go/v1" + +type Query struct { + // Filter by fields + OpIDs []int64 + PlanID *string + RepoGUID *string + DeprecatedRepoID *string // Deprecated: use RepoGUID instead + SnapshotID *string + FlowID *int64 + InstanceID *string + OriginalID *int64 + OriginalFlowID *int64 + + // Pagination + Limit int + Offset int + Reversed bool + + opIDmap map[int64]struct{} +} + +func (q Query) SetOpIDs(opIDs []int64) Query { + q.OpIDs = opIDs + return q +} + +func (q Query) SetPlanID(planID string) Query { + q.PlanID = &planID + return q +} + +func (q Query) SetRepoGUID(repoGUID string) Query { + q.RepoGUID = &repoGUID + return q +} + +func (q Query) SetSnapshotID(snapshotID string) Query { + q.SnapshotID = &snapshotID + return q +} + +func (q Query) SetFlowID(flowID int64) Query { + q.FlowID = &flowID + return q +} + +func (q Query) SetInstanceID(instanceID string) Query { + q.InstanceID = &instanceID + return q +} + +func (q Query) SetOriginalID(originalID int64) Query { + q.OriginalID = &originalID + return q +} + +func (q Query) SetOriginalFlowID(originalFlowID int64) Query { + q.OriginalFlowID = &originalFlowID + return q +} + +func (q Query) SetLimit(limit int) Query { + q.Limit = limit + return q +} + +func (q Query) SetOffset(offset int) Query { + q.Offset = offset + return q +} + +func (q Query) SetReversed(reversed bool) Query { + q.Reversed = reversed + return q +} + +var SelectAll = Query{} + +func (q *Query) buildOpIDMap() { + if len(q.OpIDs) != len(q.opIDmap) { + q.opIDmap = make(map[int64]struct{}, len(q.OpIDs)) + for _, opID := range q.OpIDs { + q.opIDmap[opID] = struct{}{} + } + } +} + +func (q *Query) Match(op *v1.Operation) bool { + if len(q.OpIDs) > 0 { + q.buildOpIDMap() + if _, ok := q.opIDmap[op.Id]; !ok { + return false + } + } + + if q.InstanceID != nil && op.InstanceId != *q.InstanceID { + return false + } + + if q.PlanID != nil && op.PlanId != *q.PlanID { + return false + } + + if q.RepoGUID != nil && op.RepoGuid != *q.RepoGUID { + return false + } + + if q.SnapshotID != nil && op.SnapshotId != *q.SnapshotID { + return false + } + + if q.FlowID != nil && op.FlowId != *q.FlowID { + return false + } + + if q.OriginalID != nil && op.OriginalId != *q.OriginalID { + return false + } + + if q.OriginalFlowID != nil && op.OriginalFlowId != *q.OriginalFlowID { + return false + } + + return true +} diff --git a/internal/oplog/randmodno.go b/internal/oplog/randmodno.go new file mode 100644 index 000000000..5a98b395f --- /dev/null +++ b/internal/oplog/randmodno.go @@ -0,0 +1,24 @@ +package oplog + +import ( + rand "math/rand/v2" + "sync" + + "github.com/garethgeorge/backrest/internal/cryptoutil" +) + +// setup a fast random number generator seeded with cryptographic randomness. +var mu sync.Mutex +var pgcRand = rand.NewPCG(cryptoutil.MustRandomUint64(), cryptoutil.MustRandomUint64()) +var randGen = rand.New(pgcRand) + +func NewRandomModno(lastModno int64) int64 { + mu.Lock() + defer mu.Unlock() + for { + modno := randGen.Int64() + if modno != lastModno { + return modno + } + } +} diff --git a/internal/oplog/sqlitestore/migrations.go b/internal/oplog/sqlitestore/migrations.go new file mode 100644 index 000000000..493d2d9cf --- /dev/null +++ b/internal/oplog/sqlitestore/migrations.go @@ -0,0 +1,171 @@ +package sqlitestore + +import ( + "fmt" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +const sqlSchemaVersion = 4 + +var sqlSchema = fmt.Sprintf(` +PRAGMA user_version = %d; + +CREATE TABLE IF NOT EXISTS system_info (version INTEGER NOT NULL); +INSERT INTO system_info (version) +SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); + +CREATE TABLE operations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ogid INTEGER NOT NULL, + original_id INTEGER NOT NULL, + original_flow_id INTEGER NOT NULL, + modno INTEGER NOT NULL, + flow_id INTEGER NOT NULL, + start_time_ms INTEGER NOT NULL, + status INTEGER NOT NULL, + snapshot_id STRING NOT NULL, + operation BLOB NOT NULL, + FOREIGN KEY (ogid) REFERENCES operation_groups (ogid) +); +CREATE INDEX operation_ogid ON operations (ogid); +CREATE INDEX operation_snapshot_id ON operations (snapshot_id); +CREATE INDEX operation_flow_id ON operations (flow_id); +CREATE INDEX operation_start_time_ms ON operations (start_time_ms); +CREATE INDEX operation_original_id ON operations (ogid, original_id); +CREATE INDEX operation_original_flow_id ON operations (ogid, original_flow_id); + +CREATE TABLE operation_groups ( + ogid INTEGER PRIMARY KEY AUTOINCREMENT, + instance_id STRING NOT NULL, + repo_guid STRING NOT NULL, + repo_id STRING NOT NULL, + plan_id STRING NOT NULL +); +CREATE INDEX group_repo_instance ON operation_groups (repo_id, instance_id); +CREATE INDEX group_repo_guid ON operation_groups (repo_guid); +CREATE INDEX group_instance ON operation_groups (instance_id); +`, sqlSchemaVersion) + +func applySqliteMigrations(store *SqliteStore, conn *sqlite.Conn) error { + var version int + if err := sqlitex.ExecuteTransient(conn, "PRAGMA user_version", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + version = stmt.ColumnInt(0) + return nil + }, + }); err != nil { + return fmt.Errorf("getting database schema version: %w", err) + } + + if version == sqlSchemaVersion { + return nil + } + + zap.S().Infof("applying oplog sqlite schema migration from storage schema %d to %d", version, sqlSchemaVersion) + + if err := withSqliteTransaction(conn, func() error { + // Check if operations table exists and rename it + var hasOperationsTable bool + if err := sqlitex.ExecuteTransient(conn, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='operations'", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + hasOperationsTable = stmt.ColumnInt(0) > 0 + return nil + }, + }); err != nil { + return fmt.Errorf("checking for operations table: %w", err) + } + + if hasOperationsTable { + zap.S().Info("renaming existing operations table to operations_old as a backup") + if err := sqlitex.ExecuteTransient(conn, "ALTER TABLE operations RENAME TO operations_old", nil); err != nil { + return fmt.Errorf("renaming operations table: %w", err) + } + + // drop all tables that aren't operations_old + drop_tables := []string{ + "operation_groups", + "operations", + } + for _, table := range drop_tables { + if err := sqlitex.ExecuteTransient(conn, fmt.Sprintf("DROP TABLE IF EXISTS %s", table), nil); err != nil { + return fmt.Errorf("dropping table %s: %w", table, err) + } + } + + // drop all indexes + indexes := []string{} + if err := sqlitex.ExecuteTransient(conn, "SELECT name FROM sqlite_master WHERE type='index'", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + indexes = append(indexes, stmt.ColumnText(0)) + return nil + }, + }); err != nil { + return fmt.Errorf("dropping indexes: %w", err) + } + for _, index := range indexes { + if err := sqlitex.ExecuteTransient(conn, fmt.Sprintf("DROP INDEX IF EXISTS %s", index), nil); err != nil { + return fmt.Errorf("dropping index %s: %w", index, err) + } + } + } + + // Apply the new schema + if err := sqlitex.ExecuteScript(conn, sqlSchema, &sqlitex.ExecOptions{}); err != nil { + return fmt.Errorf("applying schema: %w", err) + } + + // Copy data from old table if it exists + if hasOperationsTable { + var ops []*v1.Operation + batchInsert := func() error { + if err := store.addInternal(conn, ops...); err != nil { + return err + } + ops = nil + return nil + } + + if err := sqlitex.ExecuteTransient(conn, "SELECT operation FROM operations_old", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + var op v1.Operation + bytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.ColumnBytes(0, bytes) + bytes = bytes[:n] + if err := proto.Unmarshal(bytes, &op); err != nil { + return fmt.Errorf("unmarshal operation: %v", err) + } + + ops = append(ops, &op) + if len(ops) >= 512 { + if err := batchInsert(); err != nil { + return err + } + } + return nil + }, + }); err != nil { + return fmt.Errorf("copying data from old table: %w", err) + } + + if len(ops) > 0 { + if err := batchInsert(); err != nil { + return fmt.Errorf("copying data from old table: %w", err) + } + } + + if err := sqlitex.ExecuteTransient(conn, "DROP TABLE operations_old", nil); err != nil { + return fmt.Errorf("dropping old table: %w", err) + } + } + + return nil + }); err != nil { + return fmt.Errorf("migrate sqlite schema from version %d to %d: %w", version, sqlSchemaVersion, err) + } + return nil +} diff --git a/internal/oplog/sqlitestore/migrations_test.go b/internal/oplog/sqlitestore/migrations_test.go new file mode 100644 index 000000000..4c8b37e28 --- /dev/null +++ b/internal/oplog/sqlitestore/migrations_test.go @@ -0,0 +1,82 @@ +package sqlitestore + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/testutil" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestNewSqliteStore(t *testing.T) { + tempDir := t.TempDir() + store, err := NewSqliteStore(tempDir + "/test.sqlite") + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + t.Cleanup(func() { store.Close() }) +} + +func TestMigrateExisting(t *testing.T) { + tempDir := t.TempDir() + + testOps := []*v1.Operation{} + for i := 0; i < 10; i++ { + testOps = append(testOps, testutil.RandomOperation()) + } + + store, err := NewSqliteStore(tempDir + "/test.sqlite") + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + + // insert some test data + if err := store.Add(testOps...); err != nil { + t.Fatalf("error adding test data: %s", err) + } + + gotOps := make([]*v1.Operation, 0) + if err := store.Query(oplog.Query{}, func(op *v1.Operation) error { + gotOps = append(gotOps, op) + return nil + }); err != nil { + t.Fatalf("error querying sqlite store: %s", err) + } + + if len(gotOps) != len(testOps) { + t.Errorf("first check before migrations, expected %d operations, got %d", len(testOps), len(gotOps)) + } + + if err := store.Close(); err != nil { + t.Fatalf("error closing sqlite store: %s", err) + } + + // re-open the store + store2, err := NewSqliteStore(tempDir + "/test.sqlite") + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + + gotOps = gotOps[:0] + if err := store2.Query(oplog.Query{}, func(op *v1.Operation) error { + gotOps = append(gotOps, op) + return nil + }); err != nil { + t.Fatalf("error querying sqlite store: %s", err) + } + + if len(gotOps) != len(testOps) { + t.Errorf("expected %d operations, got %d", len(testOps), len(gotOps)) + } + + if diff := cmp.Diff( + &v1.OperationList{Operations: gotOps}, + &v1.OperationList{Operations: testOps}, + protocmp.Transform()); diff != "" { + t.Errorf("unexpected diff in operations back after migration: %v", diff) + } + + t.Cleanup(func() { store2.Close() }) +} diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go new file mode 100644 index 000000000..57f6b0063 --- /dev/null +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -0,0 +1,586 @@ +package sqlitestore + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/ioutil" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + lru "github.com/hashicorp/golang-lru/v2" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + "github.com/gofrs/flock" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +var ErrLocked = errors.New("sqlite db is locked") + +type SqliteStore struct { + dbpool *sqlitex.Pool + lastIDVal atomic.Int64 + dblock *flock.Flock + + ogidCache *lru.TwoQueueCache[opGroupInfo, int64] + + tidyGroupsOnce sync.Once +} + +var _ oplog.OpStore = (*SqliteStore)(nil) + +func NewSqliteStore(db string) (*SqliteStore, error) { + if err := os.MkdirAll(filepath.Dir(db), 0700); err != nil { + return nil, fmt.Errorf("create sqlite db directory: %v", err) + } + dbpool, err := sqlitex.NewPool(db, sqlitex.PoolOptions{ + PoolSize: 16, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, + }) + if err != nil { + return nil, fmt.Errorf("open sqlite pool: %v", err) + } + ogidCache, _ := lru.New2Q[opGroupInfo, int64](128) + store := &SqliteStore{ + dbpool: dbpool, + dblock: flock.New(db + ".lock"), + ogidCache: ogidCache, + } + if locked, err := store.dblock.TryLock(); err != nil { + return nil, fmt.Errorf("lock sqlite db: %v", err) + } else if !locked { + return nil, ErrLocked + } + if err := store.init(); err != nil { + return nil, err + } + return store, nil +} + +func NewMemorySqliteStore() (*SqliteStore, error) { + dbpool, err := sqlitex.NewPool("file:"+cryptoutil.MustRandomID(64)+"?mode=memory&cache=shared", sqlitex.PoolOptions{ + PoolSize: 16, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI, + }) + if err != nil { + return nil, fmt.Errorf("open sqlite pool: %v", err) + } + ogidCache, _ := lru.New2Q[opGroupInfo, int64](128) + store := &SqliteStore{ + dbpool: dbpool, + ogidCache: ogidCache, + } + if err := store.init(); err != nil { + return nil, err + } + return store, nil +} + +func (m *SqliteStore) Close() error { + if m.dblock != nil { + if err := m.dblock.Unlock(); err != nil { + return fmt.Errorf("unlock sqlite db: %v", err) + } + } + return m.dbpool.Close() +} + +func (m *SqliteStore) init() error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + defer m.dbpool.Put(conn) + + if err := applySqliteMigrations(m, conn); err != nil { + return err + } + + if err := sqlitex.ExecuteTransient(conn, "SELECT operations.id FROM operations ORDER BY operations.id DESC LIMIT 1", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + m.lastIDVal.Store(stmt.GetInt64("id")) + return nil + }, + }); err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + + return nil +} + +func (m *SqliteStore) Version() (int64, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return 0, fmt.Errorf("get version: %v", err) + } + defer m.dbpool.Put(conn) + + var version int64 + if err := sqlitex.ExecuteTransient(conn, "SELECT version FROM system_info", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + version = stmt.GetInt64("version") + return nil + }, + }); err != nil { + return 0, fmt.Errorf("get version: %v", err) + } + return version, nil +} + +func (m *SqliteStore) SetVersion(version int64) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("set version: %v", err) + } + defer m.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "UPDATE system_info SET version = ?", &sqlitex.ExecOptions{ + Args: []any{version}, + }); err != nil { + return fmt.Errorf("set version: %v", err) + } + return nil +} + +func (m *SqliteStore) buildQueryWhereClause(q oplog.Query, includeSelectClauses bool) (string, []any) { + query := make([]string, 0, 8) + args := make([]any, 0, 8) + + query = append(query, " 1=1 ") + + if q.PlanID != nil { + query = append(query, " AND operation_groups.plan_id = ?") + args = append(args, *q.PlanID) + } + if q.RepoGUID != nil { + query = append(query, " AND operation_groups.repo_guid = ?") + args = append(args, *q.RepoGUID) + } + if q.DeprecatedRepoID != nil { + query = append(query, " AND operation_groups.repo_id = ?") + args = append(args, *q.DeprecatedRepoID) + } + if q.InstanceID != nil { + query = append(query, " AND operation_groups.instance_id = ?") + args = append(args, *q.InstanceID) + } + if q.SnapshotID != nil { + query = append(query, " AND operations.snapshot_id = ?") + args = append(args, *q.SnapshotID) + } + if q.FlowID != nil { + query = append(query, " AND operations.flow_id = ?") + args = append(args, *q.FlowID) + } + if q.OriginalID != nil { + query = append(query, " AND operations.original_id = ?") + args = append(args, *q.OriginalID) + } + if q.OriginalFlowID != nil { + query = append(query, " AND operations.original_flow_id = ?") + args = append(args, *q.OriginalFlowID) + } + if q.OpIDs != nil { + query = append(query, " AND operations.id IN (") + for i, id := range q.OpIDs { + if i > 0 { + query = append(query, ",") + } + query = append(query, "?") + args = append(args, id) + } + query = append(query, ")") + } + + if includeSelectClauses { + if q.Reversed { + query = append(query, " ORDER BY operations.start_time_ms DESC, operations.id DESC") + } else { + query = append(query, " ORDER BY operations.start_time_ms ASC, operations.id ASC") + } + + if q.Limit > 0 { + query = append(query, " LIMIT ?") + args = append(args, q.Limit) + } else { + query = append(query, " LIMIT -1") + } + + if q.Offset > 0 { + query = append(query, " OFFSET ?") + args = append(args, q.Offset) + } + } + + return strings.Join(query, "")[1:], args +} + +func (m *SqliteStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("query: %v", err) + } + defer m.dbpool.Put(conn) + + where, args := m.buildQueryWhereClause(q, true) + if err := sqlitex.ExecuteTransient(conn, "SELECT operations.operation FROM operations JOIN operation_groups ON operations.ogid = operation_groups.ogid WHERE "+where, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.ColumnBytes(0, opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + return f(&op) + }, + }); err != nil && !errors.Is(err, oplog.ErrStopIteration) { + return err + } + return nil +} + +func (m *SqliteStore) QueryMetadata(q oplog.Query, f func(oplog.OpMetadata) error) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("query metadata: %v", err) + } + defer m.dbpool.Put(conn) + + where, args := m.buildQueryWhereClause(q, false) + if err := sqlitex.ExecuteTransient(conn, "SELECT operations.id, operations.modno, operations.original_id, operations.flow_id, operations.original_flow_id FROM operations JOIN operation_groups ON operations.ogid = operation_groups.ogid WHERE "+where, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + return f(oplog.OpMetadata{ + ID: stmt.ColumnInt64(0), + Modno: stmt.ColumnInt64(1), + OriginalID: stmt.ColumnInt64(2), + FlowID: stmt.ColumnInt64(3), + OriginalFlowID: stmt.ColumnInt64(4), + }) + }, + }); err != nil && !errors.Is(err, oplog.ErrStopIteration) { + return err + } + return nil +} + +// tidyGroups deletes operation groups that are no longer referenced, it takes an int64 specifying the maximum group ID to consider. +// this allows ignoring newly created groups that may not yet be referenced. +func (m *SqliteStore) tidyGroups(conn *sqlite.Conn, eligibleIDsBelow int64) { + err := sqlitex.ExecuteTransient(conn, "DELETE FROM operation_groups WHERE ogid NOT IN (SELECT DISTINCT ogid FROM operations WHERE ogid < ?)", &sqlitex.ExecOptions{ + Args: []any{eligibleIDsBelow}, + }) + if err != nil { + zap.S().Warnf("tidy groups: %v", err) + } +} + +func (m *SqliteStore) findOrCreateGroup(conn *sqlite.Conn, op *v1.Operation) (ogid int64, err error) { + ogidKey := groupInfoForOp(op) + if cachedOGID, ok := m.ogidCache.Get(ogidKey); ok { + return cachedOGID, nil + } + + var found bool + if err := sqlitex.Execute(conn, "SELECT ogid FROM operation_groups WHERE instance_id = ? AND repo_id = ? AND plan_id = ? AND repo_guid = ? LIMIT 1", &sqlitex.ExecOptions{ + Args: []any{op.InstanceId, op.RepoId, op.PlanId, op.RepoGuid}, + ResultFunc: func(stmt *sqlite.Stmt) error { + ogid = stmt.ColumnInt64(0) + found = true + return nil + }, + }); err != nil { + return 0, fmt.Errorf("find operation group: %v", err) + } + + if !found { + if err := sqlitex.Execute(conn, "INSERT INTO operation_groups (instance_id, repo_id, plan_id, repo_guid) VALUES (?, ?, ?, ?) RETURNING ogid", &sqlitex.ExecOptions{ + Args: []any{op.InstanceId, op.RepoId, op.PlanId, op.RepoGuid}, + ResultFunc: func(stmt *sqlite.Stmt) error { + ogid = stmt.ColumnInt64(0) + return nil + }, + }); err != nil { + return 0, fmt.Errorf("insert operation group: %v", err) + } + } + + m.ogidCache.Add(ogidKey, ogid) + return ogid, nil +} + +func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("transform: %v", err) + } + defer m.dbpool.Put(conn) + + where, args := m.buildQueryWhereClause(q, true) + return withImmediateSqliteTransaction(conn, func() error { + return sqlitex.ExecuteTransient(conn, "SELECT operations.operation FROM operations JOIN operation_groups ON operations.ogid = operation_groups.ogid WHERE "+where, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.ColumnBytes(0, opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + + newOp, err := f(&op) + if err != nil { + return err + } else if newOp == nil { + return nil + } + + newOp.Modno = oplog.NewRandomModno(op.Modno) + + return m.updateInternal(conn, newOp) + }, + }) + }) +} + +func (m *SqliteStore) addInternal(conn *sqlite.Conn, op ...*v1.Operation) error { + for _, o := range op { + ogid, err := m.findOrCreateGroup(conn, o) + if err != nil { + return fmt.Errorf("find ogid: %v", err) + } + + query := `INSERT INTO operations + (id, ogid, original_id, original_flow_id, modno, flow_id, start_time_ms, status, snapshot_id, operation) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + + if err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + o.Id, ogid, o.OriginalId, o.OriginalFlowId, o.Modno, o.FlowId, + o.UnixTimeStartMs, int64(o.Status), o.SnapshotId, bytes, + }, + }); err != nil { + if sqlite.ErrCode(err) == sqlite.ResultConstraintUnique { + return fmt.Errorf("operation already exists %v: %w", o.Id, oplog.ErrExist) + } + return fmt.Errorf("add operation: %v", err) + } + } + return nil +} + +func (m *SqliteStore) Add(op ...*v1.Operation) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("add operation: %v", err) + } + defer m.dbpool.Put(conn) + + return withImmediateSqliteTransaction(conn, func() error { + for _, o := range op { + o.Id = m.lastIDVal.Add(1) + if o.FlowId == 0 { + o.FlowId = o.Id + } + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + } + + return m.addInternal(conn, op...) + }) +} + +func (m *SqliteStore) Update(op ...*v1.Operation) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("update operation: %v", err) + } + defer m.dbpool.Put(conn) + + return withImmediateSqliteTransaction(conn, func() error { + return m.updateInternal(conn, op...) + }) +} + +func (m *SqliteStore) updateInternal(conn *sqlite.Conn, op ...*v1.Operation) error { + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + + ogid, err := m.findOrCreateGroup(conn, o) + if err != nil { + return fmt.Errorf("find ogid: %v", err) + } + + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, ogid = ?, start_time_ms = ?, flow_id = ?, snapshot_id = ?, modno = ?, original_id = ?, original_flow_id = ?, status = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{bytes, ogid, o.UnixTimeStartMs, o.FlowId, o.SnapshotId, o.Modno, o.OriginalId, o.OriginalFlowId, int64(o.Status), o.Id}, + }); err != nil { + return fmt.Errorf("update operation: %v", err) + } + if conn.Changes() == 0 { + return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) + } + } + return nil +} + +func (m *SqliteStore) Get(opID int64) (*v1.Operation, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("get operation: %v", err) + } + defer m.dbpool.Put(conn) + + var found bool + var opBytes []byte + if err := sqlitex.Execute(conn, "SELECT operation FROM operations WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{opID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + opBytes = make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + return nil + }, + }); err != nil { + return nil, fmt.Errorf("get operation: %v", err) + } + if !found { + return nil, oplog.ErrNotExist + } + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return nil, fmt.Errorf("unmarshal operation bytes: %v", err) + } + + return &op, nil +} + +func (m *SqliteStore) Delete(opID ...int64) ([]*v1.Operation, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("delete operation: %v", err) + } + defer m.dbpool.Put(conn) + + ops := make([]*v1.Operation, 0, len(opID)) + return ops, withImmediateSqliteTransaction(conn, func() error { + for _, batch := range ioutil.Batchify(opID, ioutil.DefaultBatchSize) { + // Optimize for the case of 1 element or batch size elements (which will be common) + useTransient := len(batch) != ioutil.DefaultBatchSize || len(batch) == 1 + batchOps, err := m.deleteHelper(conn, useTransient, batch...) + if err != nil { + return err + } + ops = append(ops, batchOps...) + } + return nil + }) +} + +func (m *SqliteStore) deleteHelper(conn *sqlite.Conn, transient bool, opID ...int64) ([]*v1.Operation, error) { + // fetch all the operations we're about to delete + predicate := []string{"operations.id IN ("} + args := []any{} + for i, id := range opID { + if i > 0 { + predicate = append(predicate, ",") + } + predicate = append(predicate, "?") + args = append(args, id) + } + predicate = append(predicate, ")") + predicateStr := strings.Join(predicate, "") + + var ops []*v1.Operation + if err := sqlitex.ExecuteTransient(conn, "SELECT operations.operation FROM operations JOIN operation_groups ON operations.ogid = operation_groups.ogid WHERE "+predicateStr, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + ops = append(ops, &op) + return nil + }, + }); err != nil { + return nil, fmt.Errorf("load operations for delete: %v", err) + } + + if len(ops) != len(opID) { + return nil, fmt.Errorf("couldn't find all operations to delete: %w", oplog.ErrNotExist) + } + + // Delete the operations + execFunc := sqlitex.Execute + if transient { + execFunc = sqlitex.ExecuteTransient + } + if err := execFunc(conn, "DELETE FROM operations WHERE "+predicateStr, &sqlitex.ExecOptions{ + Args: args, + }); err != nil { + return nil, fmt.Errorf("delete operations: %v", err) + } + + return ops, nil +} + +func (m *SqliteStore) ResetForTest(t *testing.T) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("reset for test: %v", err) + } + defer m.dbpool.Put(conn) + + if err := sqlitex.Execute(conn, "DELETE FROM operations", &sqlitex.ExecOptions{}); err != nil { + return fmt.Errorf("reset for test: %v", err) + } + m.lastIDVal.Store(0) + return nil +} + +type opGroupInfo struct { + repo string + repoGuid string + plan string + inst string +} + +func groupInfoForOp(op *v1.Operation) opGroupInfo { + return opGroupInfo{ + repo: op.RepoId, + repoGuid: op.RepoGuid, + plan: op.PlanId, + inst: op.InstanceId, + } +} diff --git a/internal/oplog/sqlitestore/sqlutil.go b/internal/oplog/sqlitestore/sqlutil.go new file mode 100644 index 000000000..b4a3d134a --- /dev/null +++ b/internal/oplog/sqlitestore/sqlutil.go @@ -0,0 +1,37 @@ +package sqlitestore + +import ( + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +// withSqliteTransaction should be used when the function only executes reads +func withSqliteTransaction(conn *sqlite.Conn, f func() error) error { + var err error + endFunc := sqlitex.Transaction(conn) + err = f() + endFunc(&err) + return err +} + +func withImmediateSqliteTransaction(conn *sqlite.Conn, f func() error) error { + var err error + endFunc, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return err + } + err = f() + endFunc(&err) + return err +} + +func withExclusiveSqliteTransaction(conn *sqlite.Conn, f func() error) error { + var err error + endFunc, err := sqlitex.ExclusiveTransaction(conn) + if err != nil { + return err + } + err = f() + endFunc(&err) + return err +} diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go new file mode 100644 index 000000000..10f12a8ed --- /dev/null +++ b/internal/oplog/storetests/storecontract_test.go @@ -0,0 +1,949 @@ +package conformance + +import ( + "fmt" + "slices" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "github.com/garethgeorge/backrest/internal/oplog/memstore" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +const ( + snapshotId = "1234567890123456789012345678901234567890123456789012345678901234" + snapshotId2 = "abcdefgh01234567890123456789012345678901234567890123456789012345" +) + +func StoresForTest(t testing.TB) map[string]oplog.OpStore { + bboltstore, err := bboltstore.NewBboltStore(t.TempDir() + "/test.boltdb") + if err != nil { + t.Fatalf("error creating bbolt store: %s", err) + } + t.Cleanup(func() { bboltstore.Close() }) + + sqlitestoreinst, err := sqlitestore.NewSqliteStore(t.TempDir() + "/test.sqlite") + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + t.Cleanup(func() { sqlitestoreinst.Close() }) + + sqlitememstore, err := sqlitestore.NewMemorySqliteStore() + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + t.Cleanup(func() { sqlitememstore.Close() }) + + return map[string]oplog.OpStore{ + "bbolt": bboltstore, + "memory": memstore.NewMemStore(), + "sqlite": sqlitestoreinst, + "sqlitemem": sqlitememstore, + } +} + +func TestCreate(t *testing.T) { + // t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + _, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + }) + } +} + +func TestListAll(t *testing.T) { + // t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + store, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + + opsToAdd := []*v1.Operation{ + { + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }, + { + UnixTimeStartMs: 4567, + PlanId: "plan2", + RepoId: "repo2", + RepoGuid: "repo2", + InstanceId: "instance2", + Op: &v1.Operation_OperationBackup{}, + }, + } + + for _, op := range opsToAdd { + if err := store.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + var ops []*v1.Operation + if err := store.Query(oplog.Query{}, func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); err != nil { + t.Fatalf("error querying operations: %s", err) + } + + if len(ops) != len(opsToAdd) { + t.Errorf("expected %d operations, got %d", len(opsToAdd), len(ops)) + } + + for i := 0; i < len(ops); i++ { + if diff := cmp.Diff(ops[i], opsToAdd[i], protocmp.Transform()); diff != "" { + t.Fatalf("unexpected diff ops[%d] != opsToAdd[%d]: %v", i, i, diff) + } + } + }) + } +} + +func TestAddOperation(t *testing.T) { + var tests = []struct { + name string + op *v1.Operation + wantErr bool + }{ + { + name: "basic operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + }, + wantErr: true, + }, + { + name: "basic backup operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + RepoGuid: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: false, + }, + { + name: "basic snapshot operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + RepoGuid: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + Op: &v1.Operation_OperationIndexSnapshot{ + OperationIndexSnapshot: &v1.OperationIndexSnapshot{ + Snapshot: &v1.ResticSnapshot{ + Id: "test", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "operation with ID", + op: &v1.Operation{ + Id: 1, + RepoId: "testrepo", + RepoGuid: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + UnixTimeStartMs: 1234, + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with repo only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + RepoGuid: "testrepo", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with plan only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "testplan", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with instance only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + InstanceId: "testinstance", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + } + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + op := proto.Clone(tc.op).(*v1.Operation) + if err := log.Add(op); (err != nil) != tc.wantErr { + t.Errorf("Add() error = %v, wantErr %v", err, tc.wantErr) + } + if !tc.wantErr { + if op.Id == 0 { + t.Errorf("Add() did not set op ID") + } + } + }) + } + }) + } +} + +func TestListOperation(t *testing.T) { + // t.Parallel() + + // these should get assigned IDs 1-3 respectively by the oplog + ops := []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + DisplayMessage: "op1", + Op: &v1.Operation_OperationBackup{}, + }, + { + InstanceId: "bar", + PlanId: "plan1", + RepoId: "repo2", + RepoGuid: "repo2", + UnixTimeStartMs: 1234, + DisplayMessage: "op2", + Op: &v1.Operation_OperationBackup{}, + }, + { + InstanceId: "baz", + PlanId: "plan2", + RepoId: "repo2", + RepoGuid: "repo2", + UnixTimeStartMs: 1234, + DisplayMessage: "op3", + FlowId: 943, + Op: &v1.Operation_OperationBackup{}, + }, + { + InstanceId: "foo", + PlanId: "foo-plan", + RepoId: "foo-repo", + RepoGuid: "foo-repo-guid", + UnixTimeStartMs: 1234, + DisplayMessage: "foo-op", + Op: &v1.Operation_OperationBackup{}, + OriginalId: 4567, + OriginalFlowId: 789, + }, + } + + tests := []struct { + name string + query oplog.Query + expected []string + }{ + { + name: "list plan1", + query: oplog.Query{}.SetPlanID("plan1"), + expected: []string{"op1", "op2"}, + }, + { + name: "list plan1 with limit", + query: oplog.Query{}.SetPlanID("plan1").SetLimit(1), + expected: []string{"op1"}, + }, + { + name: "list plan1 with offset", + query: oplog.Query{}.SetPlanID("plan1").SetOffset(1), + expected: []string{"op2"}, + }, + { + name: "list plan1 reversed", + query: oplog.Query{}.SetPlanID("plan1").SetReversed(true), + expected: []string{"op2", "op1"}, + }, + { + name: "list plan2", + query: oplog.Query{}.SetPlanID("plan2"), + expected: []string{"op3"}, + }, + { + name: "list repo1", + query: oplog.Query{}.SetRepoGUID("repo1"), + expected: []string{"op1"}, + }, + { + name: "list repo2", + query: oplog.Query{}.SetRepoGUID("repo2"), + expected: []string{"op2", "op3"}, + }, + { + name: "list flow 943", + query: oplog.Query{}.SetFlowID(943), + expected: []string{ + "op3", + }, + }, + { + name: "list original ID", + query: oplog.Query{}.SetOriginalID(4567), + expected: []string{ + "foo-op", + }, + }, + { + name: "list original flow ID", + query: oplog.Query{}.SetOriginalFlowID(789), + expected: []string{ + "foo-op", + }, + }, + { + name: "a very compound query", + query: oplog.Query{}. + SetPlanID("foo-plan"). + SetRepoGUID("foo-repo-guid"). + SetInstanceID("foo"). + SetOriginalID(4567). + SetOriginalFlowID(789), + expected: []string{ + "foo-op", + }, + }, + } + + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + for _, op := range ops { + if err := log.Add(proto.Clone(op).(*v1.Operation)); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var ops []*v1.Operation + var err error + collect := func(op *v1.Operation) error { + ops = append(ops, op) + return nil + } + err = log.Query(tc.query, collect) + if err != nil { + t.Fatalf("error listing operations: %s", err) + } + got := collectMessages(ops) + if slices.Compare(got, tc.expected) != 0 { + t.Errorf("want operations: %v, got unexpected operations: %v", tc.expected, got) + } + }) + } + }) + } +} + +func TestBigIO(t *testing.T) { + t.Parallel() + + count := 10 + + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + t.Parallel() + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + for i := 0; i < count; i++ { + if err := log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + countByPlanHelper(t, log, "plan1", count) + countByRepoGUIDHelper(t, log, "repo1", count) + }) + } +} + +func TestIndexSnapshot(t *testing.T) { + t.Parallel() + + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + SnapshotId: snapshotId, + Op: &v1.Operation_OperationIndexSnapshot{}, + } + + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + t.Parallel() + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + op := proto.Clone(op).(*v1.Operation) + + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + + var ops []*v1.Operation + if err := log.Query(oplog.Query{}.SetSnapshotID(snapshotId), func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if len(ops) != 1 { + t.Fatalf("want 1 operation, got %d", len(ops)) + } + if ops[0].Id != op.Id { + t.Errorf("want operation ID %d, got %d", op.Id, ops[0].Id) + } + }) + } +} + +func TestUpdateOperation(t *testing.T) { + t.Parallel() + + // Insert initial operation + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "oldplan", + RepoId: "oldrepo", + RepoGuid: "oldrepo", + InstanceId: "instance1", + SnapshotId: snapshotId, + } + + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + t.Parallel() + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + op := proto.Clone(op).(*v1.Operation) + + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + opId := op.Id + + // Validate initial values are indexed + countByPlanHelper(t, log, "oldplan", 1) + countByRepoGUIDHelper(t, log, "oldrepo", 1) + countBySnapshotIdHelper(t, log, snapshotId, 1) + + // Update indexed values + op.SnapshotId = snapshotId2 + op.PlanId = "myplan" + op.RepoId = "myrepo" + op.RepoGuid = "myrepo" + if err := log.Update(op); err != nil { + t.Fatalf("error updating operation: %s", err) + } + + // Validate updated values are indexed + if opId != op.Id { + t.Errorf("want operation ID %d, got %d", opId, op.Id) + } + + countByPlanHelper(t, log, "myplan", 1) + countByRepoGUIDHelper(t, log, "myrepo", 1) + countBySnapshotIdHelper(t, log, snapshotId2, 1) + + // Validate prior values are gone + countByPlanHelper(t, log, "oldplan", 0) + countByRepoGUIDHelper(t, log, "oldrepo", 0) + countBySnapshotIdHelper(t, log, snapshotId, 0) + }) + } +} + +func TestTransform(t *testing.T) { + ops := []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + { + InstanceId: "bar", + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + } + + tcs := []struct { + name string + f func(*v1.Operation) (*v1.Operation, error) + ops []*v1.Operation + want []*v1.Operation + query oplog.Query + }{ + { + name: "no change", + f: func(op *v1.Operation) (*v1.Operation, error) { + return nil, nil + }, + ops: ops, + want: ops, + }, + { + name: "modno incremented by copy", + f: func(op *v1.Operation) (*v1.Operation, error) { + return proto.Clone(op).(*v1.Operation), nil + }, + ops: ops, + want: ops, + }, + { + name: "change plan", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "oldplan", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + }, + { + name: "change plan with query", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: ops, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + RepoGuid: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + ops[1], + }, + query: oplog.Query{}.SetInstanceID("foo"), + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + for _, op := range tc.ops { + copy := proto.Clone(op).(*v1.Operation) + if err := log.Add(copy); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + if err := log.Transform(tc.query, tc.f); err != nil { + t.Fatalf("error transforming operations: %s", err) + } + + var got []*v1.Operation + if err := log.Query(oplog.Query{}, func(op *v1.Operation) error { + op.Id = 0 + op.FlowId = 0 + got = append(got, op) + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + + for _, op := range got { + op.Modno = 0 + } + + if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" { + t.Errorf("unexpected diff: %v", diff) + } + }) + } + }) + } +} + +func TestDelete(t *testing.T) { + t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + } + + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + + if err := log.Delete(op.Id); err != nil { + t.Fatalf("error deleting operation: %s", err) + } + + var ops []*v1.Operation + if err := log.Query(oplog.Query{}, func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); err != nil { + t.Fatalf("error querying operations: %s", err) + } + + if len(ops) != 0 { + t.Errorf("expected 0 operations after deletion, got %d", len(ops)) + } + }) + } +} + +func TestBulkDelete(t *testing.T) { + t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + + // Add 2000 operations + var ops []*v1.Operation + for i := 0; i < 2000; i++ { + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: fmt.Sprintf("plan%d", i), + RepoId: fmt.Sprintf("repo%d", i), + RepoGuid: fmt.Sprintf("repo%d", i), + InstanceId: fmt.Sprintf("instance%d", i), + Op: &v1.Operation_OperationBackup{}, + } + ops = append(ops, op) + } + + var ids []int64 + if err := log.Add(ops...); err != nil { + t.Fatalf("error adding operations: %s", err) + } + for _, op := range ops { + ids = append(ids, op.Id) + } + + // Delete all operations + err = log.Delete(ids...) + if err != nil { + t.Fatalf("error deleting operations: %s", err) + } + if len(ids) != 2000 { + t.Errorf("expected 2000 deleted operations, got %d", len(ids)) + } + + // Verify deletion + var count int + if err := log.Query(oplog.Query{}, func(op *v1.Operation) error { + count++ + return nil + }); err != nil { + t.Fatalf("error querying operations: %s", err) + } + if count != 0 { + t.Errorf("expected 0 operations after deletion, got %d", count) + } + }) + } +} + +func TestQueryMetadata(t *testing.T) { + t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + if name == "bbolt" { + t.Skip("bbolt does not support metadata") + } + + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + if err := log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1-guid", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + FlowId: 5, + OriginalId: 3, + OriginalFlowId: 4, + }); err != nil { + t.Fatalf("error adding operation: %s", err) + } + + var metadata []oplog.OpMetadata + if err := log.QueryMetadata(oplog.Query{}.SetPlanID("plan1"), func(op oplog.OpMetadata) error { + metadata = append(metadata, op) + return nil + }); err != nil { + t.Fatalf("error listing metadata: %s", err) + } + if len(metadata) != 1 { + t.Fatalf("want 1 metadata, got %d", len(metadata)) + } + + if metadata[0].Modno == 0 { + t.Errorf("modno should not be 0") + } + metadata[0].Modno = 0 // ignore for diff since it's random + + if diff := cmp.Diff(metadata[0], oplog.OpMetadata{ + ID: metadata[0].ID, + FlowID: 5, + OriginalID: 3, + OriginalFlowID: 4, + }); diff != "" { + t.Errorf("unexpected diff: %v", diff) + } + }) + } +} + +func collectMessages(ops []*v1.Operation) []string { + var messages []string + for _, op := range ops { + messages = append(messages, op.DisplayMessage) + } + return messages +} + +func countByRepoGUIDHelper(t *testing.T, log *oplog.OpLog, repoGUID string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{}.SetRepoGUID(repoGUID), func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func countByPlanHelper(t *testing.T, log *oplog.OpLog, plan string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{}.SetPlanID(plan), func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func countBySnapshotIdHelper(t *testing.T, log *oplog.OpLog, snapshotId string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{}.SetSnapshotID(snapshotId), func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func BenchmarkAdd(b *testing.B) { + for name, store := range StoresForTest(b) { + b.Run(name, func(b *testing.B) { + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } + for i := 0; i < b.N; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + }) + } +} + +func BenchmarkList(b *testing.B) { + for _, count := range []int{100, 1000, 10000} { + b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { + for name, store := range StoresForTest(b) { + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } + for i := 0; i < count; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := 0 + if err := log.Query(oplog.Query{}.SetPlanID("plan1"), func(op *v1.Operation) error { + c += 1 + return nil + }); err != nil { + b.Fatalf("error listing operations: %s", err) + } + if c != count { + b.Fatalf("want %d operations, got %d", count, c) + } + } + }) + } + }) + } +} + +func BenchmarkGetLastItem(b *testing.B) { + for _, count := range []int{100, 1000, 10000} { + b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { + for name, store := range StoresForTest(b) { + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } + for i := 0; i < count; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + RepoGuid: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := 0 + if err := log.Query(oplog.Query{}.SetPlanID("plan1").SetReversed(true), func(op *v1.Operation) error { + c += 1 + return oplog.ErrStopIteration + }); err != nil { + b.Fatalf("error listing operations: %s", err) + } + if c != 1 { + b.Fatalf("want 1 operation, got %d", c) + } + } + }) + } + }) + } +} diff --git a/internal/orchestrator/logging/logging.go b/internal/orchestrator/logging/logging.go new file mode 100644 index 000000000..69f219d91 --- /dev/null +++ b/internal/orchestrator/logging/logging.go @@ -0,0 +1,45 @@ +package logging + +import ( + "context" + "io" + + "github.com/garethgeorge/backrest/internal/ioutil" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type contextKey int + +const ( + contextKeyLogWriter contextKey = iota +) + +func WriterFromContext(ctx context.Context) io.Writer { + writer, ok := ctx.Value(contextKeyLogWriter).(io.Writer) + if !ok { + return nil + } + return writer +} + +func ContextWithWriter(ctx context.Context, logger io.Writer) context.Context { + return context.WithValue(ctx, contextKeyLogWriter, logger) +} + +// Logger returns a logger from the context, or the global logger if none is found. +// this is somewhat expensive, it should be called once per task. +func Logger(ctx context.Context, prefix string) *zap.Logger { + writer := WriterFromContext(ctx) + if writer == nil { + return zap.L() + } + p := zap.NewProductionEncoderConfig() + p.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05.000Z") + fe := zapcore.NewConsoleEncoder(p) + l := zap.New(zapcore.NewTee( + zap.L().Core(), + zapcore.NewCore(fe, zapcore.AddSync(&ioutil.LinePrefixer{W: writer, Prefix: []byte(prefix)}), zapcore.DebugLevel), + )) + return l +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go new file mode 100644 index 000000000..a067cf913 --- /dev/null +++ b/internal/orchestrator/orchestrator.go @@ -0,0 +1,772 @@ +package orchestrator + +import ( + "context" + "errors" + "fmt" + "io" + "slices" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/metric" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/garethgeorge/backrest/internal/queue" + "github.com/google/uuid" + "go.uber.org/multierr" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +var ErrRepoNotFound = errors.New("repo not found") +var ErrRepoInitializationFailed = errors.New("repo initialization failed") +var ErrPlanNotFound = errors.New("plan not found") + +const ( + defaultTaskLogDuration = 14 * 24 * time.Hour + + defaultAutoInitTimeout = 60 * time.Second +) + +// Orchestrator is responsible for managing repos and backups. +type Orchestrator struct { + mu sync.Mutex + configMgr *config.ConfigManager + config *v1.Config + OpLog *oplog.OpLog + repoPool *resticRepoPool + taskQueue *queue.TimePriorityQueue[stContainer] + logStore *logstore.LogStore + resticBin string + + taskCancelMu sync.Mutex + taskCancel map[int64]context.CancelFunc + + // now for the purpose of testing; used by Run() to get the current time. + now func() time.Time +} + +var _ tasks.TaskExecutor = &Orchestrator{} + +type stContainer struct { + tasks.ScheduledTask + retryCount int // number of times this task has been retried. + configModno int32 + callbacks []func(error) +} + +func (st stContainer) Eq(other stContainer) bool { + return st.ScheduledTask.Eq(other.ScheduledTask) +} + +func (st stContainer) Less(other stContainer) bool { + return st.ScheduledTask.Less(other.ScheduledTask) +} + +func NewOrchestrator(resticBin string, cfgMgr *config.ConfigManager, log *oplog.OpLog, logStore *logstore.LogStore) (*Orchestrator, error) { + // create the orchestrator. + o := &Orchestrator{ + OpLog: log, + configMgr: cfgMgr, + taskQueue: queue.NewTimePriorityQueue[stContainer](), + logStore: logStore, + taskCancel: make(map[int64]context.CancelFunc), + resticBin: resticBin, + } + + // verify the operation log and mark any incomplete operations as failed. + if log != nil { // oplog may be nil for testing. + incompleteRepos := []string{} + incompleteOps := []*v1.Operation{} + toDelete := []int64{} + + startTime := time.Now() + zap.S().Info("scrubbing operation log for incomplete operations") + + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED || op.Status == v1.OperationStatus_STATUS_USER_CANCELLED || op.Status == v1.OperationStatus_STATUS_UNKNOWN { + toDelete = append(toDelete, op.Id) + } else if op.Status == v1.OperationStatus_STATUS_INPROGRESS { + incompleteOps = append(incompleteOps, op) + if !slices.Contains(incompleteRepos, op.RepoId) { + incompleteRepos = append(incompleteRepos, op.RepoId) + } + } + return nil + }); err != nil { + return nil, fmt.Errorf("scan oplog: %w", err) + } + + for _, op := range incompleteOps { + op.Status = v1.OperationStatus_STATUS_ERROR + op.DisplayMessage = "Operation was incomplete when orchestrator was restarted." + op.UnixTimeEndMs = op.UnixTimeStartMs + if err := log.Update(op); err != nil { + return nil, fmt.Errorf("update incomplete operation: %w", err) + } + } + + if err := log.Delete(toDelete...); err != nil { + return nil, fmt.Errorf("delete incomplete operations: %w", err) + } + + for _, repoId := range incompleteRepos { + repo, err := o.GetRepoOrchestrator(repoId) + if err != nil { + if errors.Is(err, ErrRepoNotFound) { + zap.L().Warn("repo not found for incomplete operation. Possibly just deleted.", zap.String("repo", repoId)) + } + return nil, fmt.Errorf("get repo %q: %w", repoId, err) + } + + if err := repo.Unlock(context.Background()); err != nil { + zap.L().Error("failed to unlock repo", zap.String("repo", repoId), zap.Error(err)) + } + } + + zap.L().Info("scrubbed operation log for incomplete operations", + zap.Duration("duration", time.Since(startTime)), + zap.Int("incomplete_ops", len(incompleteOps)), + zap.Int("incomplete_repos", len(incompleteRepos)), + zap.Int("deleted_ops", len(toDelete))) + } + + // initialize any uninitialized repos. + if err := o.autoInitReposIfNeeded(resticBin); err != nil { + return nil, fmt.Errorf("auto-initialize repos: %w", err) + } + + // apply starting configuration which also queues initial tasks. + cfg, err := o.configMgr.Get() + if err != nil { + return nil, fmt.Errorf("get config: %w", err) + } + if err := o.applyConfig(cfg); err != nil { + return nil, fmt.Errorf("apply initial config: %w", err) + } + + zap.L().Info("orchestrator created") + + return o, nil +} + +func (o *Orchestrator) autoInitReposIfNeeded(resticBin string) error { + var fullErr error + cfg, err := o.configMgr.Get() + if err != nil { + return fmt.Errorf("get config: %w", err) + } + cfg = proto.Clone(cfg).(*v1.Config) + + initializedRepo := false + for _, r := range cfg.Repos { + if !r.AutoInitialize { + continue + } + zap.L().Info("auto-initializing repo", zap.String("repo", r.Id)) + rorch, err := repo.NewRepoOrchestrator(cfg, r, resticBin) + if err != nil { + fullErr = multierr.Append(fullErr, fmt.Errorf("auto-initialize repo %q: %w", r.Id, err)) + continue + } + ctx, cancel := context.WithTimeout(context.Background(), defaultAutoInitTimeout) + defer cancel() + if err := rorch.Init(ctx); err != nil { + fullErr = multierr.Append(fullErr, fmt.Errorf("auto-initialize repo %q: %w", r.Id, err)) + continue + } + + guid, err := rorch.RepoGUID() + if err != nil { + return fmt.Errorf("get repo %q guid: %w", r.Id, err) + } + r.Guid = guid + r.AutoInitialize = false + initializedRepo = true + } + + if initializedRepo && fullErr == nil { + cfg.Modno++ + if err := o.configMgr.Update(cfg); err != nil { + return fmt.Errorf("update config: %w", err) + } + } + + return fullErr +} + +func (o *Orchestrator) curTime() time.Time { + if o.now != nil { + return o.now() + } + return time.Now() +} + +func (o *Orchestrator) applyConfig(cfg *v1.Config) error { + for _, repo := range cfg.Repos { + if repo.Guid == "" { + return errors.New("guid is required for all repos") + } + } + o.mu.Lock() + o.config = proto.Clone(cfg).(*v1.Config) + o.repoPool = newResticRepoPool(o.resticBin, o.config) + o.mu.Unlock() + return o.ScheduleDefaultTasks(cfg) +} + +// rescheduleTasksIfNeeded checks if any tasks need to be rescheduled based on config changes. +func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { + if o.OpLog == nil { + return nil + } + + zap.L().Info("scheduling default tasks, waiting for task queue reset.") + removedTasks := o.taskQueue.Reset() + + ids := []int64{} + for _, t := range removedTasks { + if t.Op.GetId() != 0 { + ids = append(ids, t.Op.GetId()) + } + } + if err := o.OpLog.Delete(ids...); err != nil { + zap.S().Warnf("failed to delete cancelled tasks from oplog: %v", err) + } + + zap.L().Info("reset task queue, scheduling new task set", zap.String("timezone", time.Now().Location().String())) + + // Requeue tasks that are affected by the config change. + if err := o.ScheduleTask(tasks.NewCollectGarbageTask(o.logStore), tasks.TaskPriorityDefault); err != nil { + return fmt.Errorf("schedule collect garbage task: %w", err) + } + + var repoByID = map[string]*v1.Repo{} + for _, repo := range config.Repos { + repoByID[repo.GetId()] = repo + } + + for _, plan := range config.Plans { + // Schedule a backup task for the plan + repo := repoByID[plan.Repo] + if repo == nil { + return fmt.Errorf("repo %q not found for plan %q", plan.Repo, plan.Id) + } + + t := tasks.NewScheduledBackupTask(repo, plan) + if err := o.ScheduleTask(t, tasks.TaskPriorityDefault); err != nil { + return fmt.Errorf("schedule backup task for plan %q: %w", plan.Id, err) + } + } + + for _, repo := range config.Repos { + // Schedule a prune task for the repo + t := tasks.NewPruneTask(repo, tasks.PlanForSystemTasks, false) + if err := o.ScheduleTask(t, tasks.TaskPriorityPrune); err != nil { + return fmt.Errorf("schedule prune task for repo %q: %w", repo.GetId(), err) + } + + // Schedule a check task for the repo + t = tasks.NewCheckTask(repo, tasks.PlanForSystemTasks, false) + if err := o.ScheduleTask(t, tasks.TaskPriorityCheck); err != nil { + return fmt.Errorf("schedule check task for repo %q: %w", repo.GetId(), err) + } + } + + return nil +} + +func (o *Orchestrator) GetRepoOrchestrator(repoId string) (repo *repo.RepoOrchestrator, err error) { + o.mu.Lock() + defer o.mu.Unlock() + + r, err := o.repoPool.GetRepo(repoId) + if err != nil { + return nil, fmt.Errorf("get repo %q: %w", repoId, err) + } + return r, nil +} + +func (o *Orchestrator) GetRepo(repoID string) (*v1.Repo, error) { + o.mu.Lock() + defer o.mu.Unlock() + + repo := config.FindRepo(o.config, repoID) + if repo == nil { + return nil, fmt.Errorf("get repo %q: %w", repoID, ErrRepoNotFound) + } + return repo, nil +} + +func (o *Orchestrator) GetPlan(planID string) (*v1.Plan, error) { + o.mu.Lock() + defer o.mu.Unlock() + + plan := config.FindPlan(o.config, planID) + if plan == nil { + return nil, fmt.Errorf("get plan %q: %w", planID, ErrPlanNotFound) + } + return plan, nil +} + +func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error { + allTasks := o.taskQueue.GetAll() + idx := slices.IndexFunc(allTasks, func(t stContainer) bool { + return t.Op != nil && t.Op.GetId() == operationId + }) + if idx == -1 { + o.taskCancelMu.Lock() + if cancel, ok := o.taskCancel[operationId]; ok { + cancel() + } + o.taskCancelMu.Unlock() + return nil + } + t := allTasks[idx] + + if err := o.cancelHelper(t.Op, status); err != nil { + return fmt.Errorf("cancel operation: %w", err) + } + o.taskQueue.Remove(t) + + if st, err := o.CreateUnscheduledTask(t.Task, tasks.TaskPriorityDefault, t.RunAt); err != nil { + return fmt.Errorf("reschedule cancelled task: %w", err) + } else if !st.Eq(tasks.NeverScheduledTask) { + o.taskQueue.Enqueue(st.RunAt, tasks.TaskPriorityDefault, stContainer{ + ScheduledTask: st, + configModno: o.config.Modno, + }) + } + + return nil +} + +func (o *Orchestrator) cancelHelper(op *v1.Operation, status v1.OperationStatus) error { + op.Status = status + op.UnixTimeEndMs = time.Now().UnixMilli() + if err := o.OpLog.Update(op); err != nil { + return fmt.Errorf("update cancelled operation: %w", err) + } + return nil +} + +// Run is the main orchestration loop. Cancel the context to stop the loop. +func (o *Orchestrator) Run(ctx context.Context) { + zap.L().Info("starting orchestrator loop") + + // Setup config watching + configCh := o.configMgr.Watch() + defer o.configMgr.StopWatching(configCh) + + // Start the config watcher goroutine + go o.watchConfigChanges(ctx, configCh) + + // Start the clock jump detector goroutine + go o.watchForClockJumps(ctx) + + // Main task processing loop + for { + if ctx.Err() != nil { + zap.L().Info("shutting down orchestrator loop, context cancelled.") + break + } + + t := o.taskQueue.Dequeue(ctx) + if t.Task == nil { + continue + } + + // Clone the operation in case we need to reset changes and reschedule the task for a retry + originalOp := proto.Clone(t.Op).(*v1.Operation) + o.prepareOperationForRetry(&t) + + // Execute the task + err := o.RunTask(ctx, t.ScheduledTask) + + // Handle task completion, including potential retry + if o.handleTaskCompletion(&t, err, originalOp) { + continue // Skip callbacks for retried tasks + } + + // Execute callbacks + for _, cb := range t.callbacks { + go cb(err) + } + } +} + +// watchConfigChanges handles configuration updates from the config manager +func (o *Orchestrator) watchConfigChanges(ctx context.Context, configCh <-chan struct{}) { + for { + select { + case <-ctx.Done(): + return + case <-configCh: + cfg, err := o.configMgr.Get() + if err != nil { + zap.S().Errorf("orchestrator failed to refresh config after change notification: %v", err) + continue + } + if err := o.applyConfig(cfg); err != nil { + zap.S().Errorf("orchestrator failed to apply config: %v", err) + } + } + } +} + +// watchForClockJumps detects system clock jumps and reschedules tasks when detected +func (o *Orchestrator) watchForClockJumps(ctx context.Context) { + // Watchdog timer to detect clock jumps and reschedule all tasks + interval := 5 * time.Minute + grace := 30 * time.Second + ticker := time.NewTicker(interval) + defer ticker.Stop() + + lastTickTime := time.Now() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + deltaMs := lastTickTime.Add(interval).UnixMilli() - time.Now().UnixMilli() + lastTickTime = time.Now() + if deltaMs < 0 { + deltaMs = -deltaMs + } + if deltaMs < grace.Milliseconds() { + continue + } + zap.S().Warnf("detected a clock jump, watchdog timer is off from realtime by %dms, rescheduling all tasks", deltaMs) + + if err := o.ScheduleDefaultTasks(o.config); err != nil { + zap.S().Errorf("failed to schedule default tasks: %v", err) + } + } + } +} + +// prepareOperationForRetry prepares a task's operation for a retry by updating its display message +// and cleaning up previous hook executions if necessary +func (o *Orchestrator) prepareOperationForRetry(t *stContainer) { + if t.Op == nil || t.retryCount == 0 { + return + } + + t.Op.DisplayMessage = fmt.Sprintf("running after %d retries", t.retryCount) + + // Delete any previous hook executions for this operation in case this is a retry + prevHookExecutionIDs := []int64{} + if err := o.OpLog.Query(oplog.Query{FlowID: &t.Op.FlowId}, func(op *v1.Operation) error { + if hookOp, ok := op.Op.(*v1.Operation_OperationRunHook); ok && hookOp.OperationRunHook.GetParentOp() == t.Op.Id { + prevHookExecutionIDs = append(prevHookExecutionIDs, op.Id) + } + return nil + }); err != nil { + zap.L().Error("failed to collect previous hook execution IDs", zap.Error(err)) + } + + if len(prevHookExecutionIDs) > 0 { + zap.S().Debugf("deleting previous hook execution IDs: %v", prevHookExecutionIDs) + if err := o.OpLog.Delete(prevHookExecutionIDs...); err != nil { + zap.L().Error("failed to delete previous hook execution IDs", zap.Error(err)) + } + } +} + +// handleTaskCompletion processes task completion, handling retry logic or rescheduling as needed. +// Returns true if the task was requeued for retry. +func (o *Orchestrator) handleTaskCompletion(t *stContainer, err error, originalOp *v1.Operation) bool { + // Check if config has changed since the task was scheduled + o.mu.Lock() + curCfgModno := o.config.Modno + o.mu.Unlock() + + if t.configModno != curCfgModno { + // Config has changed, don't reschedule + return false + } + + // Handle task retry logic if needed + var retryErr *tasks.TaskRetryError + if errors.As(err, &retryErr) { + o.retryTask(t, retryErr, originalOp) + return true // Skip callbacks for retried tasks + } + + // Regular rescheduling + if e := o.ScheduleTask(t.Task, tasks.TaskPriorityDefault); e != nil { + zap.L().Error("reschedule task", zap.String("task", t.Task.Name()), zap.Error(e)) + } + + return false +} + +// retryTask sets up a task for retry with backoff +func (o *Orchestrator) retryTask(t *stContainer, retryErr *tasks.TaskRetryError, originalOp *v1.Operation) { + t.retryCount++ + delay := retryErr.Backoff(t.retryCount) + + // Update operation state if present + if t.Op != nil { + t.Op = originalOp + t.Op.DisplayMessage = fmt.Sprintf("waiting for retry, current backoff delay: %v", delay) + t.Op.UnixTimeStartMs = t.RunAt.UnixMilli() + if err := o.OpLog.Update(t.Op); err != nil { + zap.S().Errorf("failed to update operation in oplog: %v", err) + } + } + + // Enqueue the task for retry + t.RunAt = time.Now().Add(delay) + o.taskQueue.Enqueue(t.RunAt, tasks.TaskPriorityDefault, *t) + + zap.L().Info("retrying task", + zap.String("task", t.Task.Name()), + zap.String("runAt", t.RunAt.Format(time.RFC3339)), + zap.Duration("delay", delay)) +} + +func (o *Orchestrator) RunTask(parentCtx context.Context, st tasks.ScheduledTask) error { + zap.L().Info("running task", zap.String("task", st.Task.Name()), zap.String("runAt", st.RunAt.Format(time.RFC3339))) + + // Set up context, logging, and cancellation + ctx, cancel := context.WithCancel(parentCtx) + defer cancel() + ctx, logWriter := o.setupTaskContext(ctx, st.Op, cancel) + defer o.cleanupTaskContext(ctx, st.Op, logWriter) + + // Run the task and record metrics + err := o.executeTask(ctx, st) + + // Update operation status based on execution result + if st.Op != nil { + o.updateOperationStatus(ctx, st.Op, err) + } + + return err +} + +// setupTaskContext prepares the context for task execution with appropriate logging and cancellation +func (o *Orchestrator) setupTaskContext(ctx context.Context, op *v1.Operation, cancel context.CancelFunc) (context.Context, io.WriteCloser) { + var logWriter io.WriteCloser + + if op != nil { + // Register cancellation function + o.taskCancelMu.Lock() + o.taskCancel[op.Id] = cancel + o.taskCancelMu.Unlock() + + // Set up logging + logID := uuid.New().String() + var err error + logWriter, err = o.logStore.Create(logID, op.Id, defaultTaskLogDuration) + if err != nil { + zap.S().Errorf("failed to create live log writer: %v", err) + } + ctx = logging.ContextWithWriter(ctx, logWriter) + + // Update operation status and log reference + op.Logref = logID + op.UnixTimeStartMs = time.Now().UnixMilli() + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_UNKNOWN { + op.Status = v1.OperationStatus_STATUS_INPROGRESS + } + + // Record the operation in the oplog + o.recordOperationInOplog(op) + } else { + // If no operation is provided, discard logs + ctx = logging.ContextWithWriter(ctx, io.Discard) + } + + return ctx, logWriter +} + +// recordOperationInOplog adds or updates an operation in the operation log +func (o *Orchestrator) recordOperationInOplog(op *v1.Operation) { + var err error + if op.Id != 0 { + err = o.OpLog.Update(op) + } else { + err = o.OpLog.Add(op) + } + + if err != nil { + zap.S().Errorf("failed to update operation in oplog: %v", err) + } +} + +// cleanupTaskContext handles cleanup after task execution +func (o *Orchestrator) cleanupTaskContext(ctx context.Context, op *v1.Operation, logWriter io.WriteCloser) { + // Remove the cancel function from the map if there was an operation + if op != nil { + o.taskCancelMu.Lock() + delete(o.taskCancel, op.Id) + o.taskCancelMu.Unlock() + } + + // Close the log writer if one was created + if logWriter != nil { + if err := logWriter.Close(); err != nil { + zap.S().Warnf("failed to close log writer, logs may be partial: %v", err) + } + } +} + +// executeTask runs the task and records metrics for it +func (o *Orchestrator) executeTask(ctx context.Context, st tasks.ScheduledTask) error { + start := time.Now() + runner := newTaskRunnerImpl(o, st.Task, st.Op) + err := st.Task.Run(ctx, st, runner) + + // Record metrics based on task result + if err != nil { + runner.Logger(ctx).Error("task failed", zap.Error(err), zap.Duration("duration", time.Since(start))) + metric.GetRegistry().RecordTaskRun(st.Task.RepoID(), st.Task.PlanID(), st.Task.Type(), time.Since(start).Seconds(), "failed") + } else { + runner.Logger(ctx).Info("task finished", zap.Duration("duration", time.Since(start))) + metric.GetRegistry().RecordTaskRun(st.Task.RepoID(), st.Task.PlanID(), st.Task.Type(), time.Since(start).Seconds(), "success") + } + + return err +} + +// updateOperationStatus updates the operation's status based on the task execution result +func (o *Orchestrator) updateOperationStatus(ctx context.Context, op *v1.Operation, err error) { + if err != nil { + // Handle different error types + var taskCancelledError *tasks.TaskCancelledError + var taskRetryError *tasks.TaskRetryError + if errors.As(err, &taskCancelledError) { + op.Status = v1.OperationStatus_STATUS_USER_CANCELLED + } else if errors.As(err, &taskRetryError) { + op.Status = v1.OperationStatus_STATUS_PENDING + } else { + op.Status = v1.OperationStatus_STATUS_ERROR + } + + // Prepend the error to the display message + if op.DisplayMessage != "" { + op.DisplayMessage = err.Error() + "\n\n" + op.DisplayMessage + } else { + op.DisplayMessage = err.Error() + } + + if ctx.Err() != nil { + op.DisplayMessage += "\n\nnote: task was interrupted by context cancellation or instance shutdown" + } + } + + // Set end time and update final status + op.UnixTimeEndMs = time.Now().UnixMilli() + if op.Status == v1.OperationStatus_STATUS_INPROGRESS { + op.Status = v1.OperationStatus_STATUS_SUCCESS + } + + // Update the operation in the log + if err := o.OpLog.Update(op); err != nil { + zap.S().Errorf("failed to update operation in oplog: %v", err) + } +} + +func (o *Orchestrator) ScheduleTask(t tasks.Task, priority int, callbacks ...func(error)) error { + nextRun, err := o.CreateUnscheduledTask(t, priority, o.curTime()) + if err != nil { + return err + } + if nextRun.Eq(tasks.NeverScheduledTask) { + return nil + } + + stc := stContainer{ + ScheduledTask: nextRun, + configModno: o.config.Modno, + callbacks: callbacks, + } + + o.taskQueue.Enqueue(nextRun.RunAt, priority, stc) + zap.L().Info("scheduled task", zap.String("task", t.Name()), zap.String("runAt", nextRun.RunAt.Format(time.RFC3339))) + return nil +} + +func (o *Orchestrator) CreateUnscheduledTask(t tasks.Task, priority int, curTime time.Time) (tasks.ScheduledTask, error) { + nextRun, err := t.Next(curTime, newTaskRunnerImpl(o, t, nil)) + if err != nil { + return tasks.NeverScheduledTask, fmt.Errorf("finding run time for task %q: %w", t.Name(), err) + } + if nextRun.Eq(tasks.NeverScheduledTask) { + return tasks.NeverScheduledTask, nil + } + nextRun.Task = t + + if nextRun.Op != nil { + nextRun.Op.InstanceId = o.config.Instance + nextRun.Op.PlanId = t.PlanID() + nextRun.Op.RepoId = t.Repo().GetId() + nextRun.Op.RepoGuid = t.Repo().GetGuid() + nextRun.Op.Status = v1.OperationStatus_STATUS_PENDING + nextRun.Op.UnixTimeStartMs = nextRun.RunAt.UnixMilli() + + if err := o.OpLog.Add(nextRun.Op); err != nil { + return tasks.NeverScheduledTask, fmt.Errorf("add operation to oplog: %w", err) + } + } + return nextRun, nil +} + +func (o *Orchestrator) Config() *v1.Config { + o.mu.Lock() + defer o.mu.Unlock() + return proto.Clone(o.config).(*v1.Config) +} + +// resticRepoPool caches restic repos. +type resticRepoPool struct { + mu sync.Mutex + resticPath string + repos map[string]*repo.RepoOrchestrator + config *v1.Config +} + +func newResticRepoPool(resticPath string, config *v1.Config) *resticRepoPool { + return &resticRepoPool{ + resticPath: resticPath, + repos: make(map[string]*repo.RepoOrchestrator), + config: config, + } +} + +func (rp *resticRepoPool) GetRepo(repoId string) (*repo.RepoOrchestrator, error) { + rp.mu.Lock() + defer rp.mu.Unlock() + + if rp.config.Repos == nil { + return nil, ErrRepoNotFound + } + + // Check if we already have a repo for this id, if we do return it. + r, ok := rp.repos[repoId] + if ok { + return r, nil + } + + repoProto := config.FindRepo(rp.config, repoId) + if repoProto == nil { + return nil, ErrRepoNotFound + } + + // Otherwise create a new repo. + r, err := repo.NewRepoOrchestrator(rp.config, repoProto, rp.resticPath) + if err != nil { + return nil, err + } + rp.repos[repoId] = r + return r, nil +} diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go new file mode 100644 index 000000000..94f01ffa2 --- /dev/null +++ b/internal/orchestrator/orchestrator_test.go @@ -0,0 +1,56 @@ +package orchestrator + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/resticinstaller" +) + +func TestAutoInitializeRepos(t *testing.T) { + t.Parallel() + + configMgr := &config.ConfigManager{ + Store: &config.MemoryStore{ + Config: &v1.Config{ + Version: 4, + Instance: "test-instance", + Repos: []*v1.Repo{ + { + Id: "test", + Uri: t.TempDir(), + Flags: []string{ + "--no-cache", + "--insecure-no-password", + }, + AutoInitialize: true, + }, + }, + }, + }, + } + + resticBin, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + t.Fatalf("failed to find or install restic binary: %v", err) + } + + _, err = NewOrchestrator(resticBin, configMgr, nil, nil) + if err != nil { + t.Fatalf("failed to create orchestrator: %v", err) + } + + if err != nil { + t.Fatalf("failed to construct orchestrator: %v", err) + } + + newConfig, _ := configMgr.Get() + + if newConfig.Repos[0].Guid == "" { + t.Fatalf("expected repo guid to be set") + } + if newConfig.Repos[0].AutoInitialize { + t.Fatalf("expected repo auto-initialize to be false") + } +} diff --git a/internal/orchestrator/repo/command_prefix.go b/internal/orchestrator/repo/command_prefix.go new file mode 100644 index 000000000..6cf3ecacb --- /dev/null +++ b/internal/orchestrator/repo/command_prefix.go @@ -0,0 +1,52 @@ +package repo + +import ( + "errors" + "os/exec" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/pkg/restic" +) + +func niceAvailable() bool { + _, err := exec.LookPath("nice") + return err == nil +} + +func ioniceAvailable() bool { + _, err := exec.LookPath("ionice") + return err == nil +} + +// resolveCommandPrefix returns a list of restic.GenericOption that should be applied to a restic command based on the given prefix. +func resolveCommandPrefix(prefix *v1.CommandPrefix) ([]restic.GenericOption, error) { + var opts []restic.GenericOption + + if prefix.GetCpuNice() != v1.CommandPrefix_CPU_DEFAULT { + if !niceAvailable() { + return nil, errors.New("nice not available, cpu_nice cannot be used") + } + switch prefix.GetCpuNice() { + case v1.CommandPrefix_CPU_HIGH: + opts = append(opts, restic.WithPrefixCommand("nice", "-n", "-10")) + case v1.CommandPrefix_CPU_LOW: + opts = append(opts, restic.WithPrefixCommand("nice", "-n", "10")) + } + } + + if prefix.GetIoNice() != v1.CommandPrefix_IO_DEFAULT { + if !ioniceAvailable() { + return nil, errors.New("ionice not available, io_nice cannot be used") + } + switch prefix.GetIoNice() { + case v1.CommandPrefix_IO_IDLE: + opts = append(opts, restic.WithPrefixCommand("ionice", "-c", "3")) // idle priority, only runs when other IO is not queued. + case v1.CommandPrefix_IO_BEST_EFFORT_LOW: + opts = append(opts, restic.WithPrefixCommand("ionice", "-c", "2", "-n", "7")) // best effort, low priority. Default is -n 4. + case v1.CommandPrefix_IO_BEST_EFFORT_HIGH: + opts = append(opts, restic.WithPrefixCommand("ionice", "-c", "2", "-n", "0")) // best effort, high(er) than default priority. Default is -n 4. + } + } + + return opts, nil +} diff --git a/internal/orchestrator/repo/env.go b/internal/orchestrator/repo/env.go new file mode 100644 index 000000000..25638e848 --- /dev/null +++ b/internal/orchestrator/repo/env.go @@ -0,0 +1,18 @@ +package repo + +import ( + "os" + "regexp" +) + +var ( + envVarSubstRegex = regexp.MustCompile(`\${[^}]*}`) +) + +// ExpandEnv expands environment variables of the form ${VAR} in a string. +func ExpandEnv(s string) string { + return envVarSubstRegex.ReplaceAllStringFunc(s, func(match string) string { + e, _ := os.LookupEnv(match[2 : len(match)-1]) + return e + }) +} diff --git a/internal/orchestrator/repo/logging.go b/internal/orchestrator/repo/logging.go new file mode 100644 index 000000000..ade3d435c --- /dev/null +++ b/internal/orchestrator/repo/logging.go @@ -0,0 +1,27 @@ +package repo + +import ( + "context" + "fmt" + + "github.com/garethgeorge/backrest/internal/ioutil" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" + "github.com/garethgeorge/backrest/pkg/restic" +) + +// pipeResticLogsToWriter sets the restic logger to write to the provided writer. +// returns a new context with the logger set and a function to flush the logs. +func forwardResticLogs(ctx context.Context) (context.Context, func()) { + writer := logging.WriterFromContext(ctx) + if writer == nil { + return ctx, func() {} + } + limitWriter := &ioutil.LimitWriter{W: writer, N: 64 * 1024} + prefixWriter := &ioutil.LinePrefixer{W: limitWriter, Prefix: []byte("[restic] ")} + return restic.ContextWithLogger(ctx, prefixWriter), func() { + if limitWriter.D > 0 { + fmt.Fprintf(prefixWriter, "... Output truncated, %d bytes dropped\n", limitWriter.D) + } + prefixWriter.Close() + } +} diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go new file mode 100644 index 000000000..c50e85599 --- /dev/null +++ b/internal/orchestrator/repo/repo.go @@ -0,0 +1,445 @@ +package repo + +import ( + "context" + "errors" + "fmt" + "io" + "path" + "runtime" + "slices" + "sort" + "strings" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" + "github.com/garethgeorge/backrest/internal/protoutil" + "github.com/garethgeorge/backrest/pkg/restic" + "github.com/google/shlex" + "go.uber.org/zap" +) + +// RepoOrchestrator implements higher level repository operations on top of +// the restic package. It can be thought of as a controller for a repo. +type RepoOrchestrator struct { + mu sync.Mutex + + config *v1.Config + repoConfig *v1.Repo + repo *restic.Repo +} + +// NewRepoOrchestrator accepts a config and a repo that is configured with the properties of that config object. +func NewRepoOrchestrator(config *v1.Config, repoConfig *v1.Repo, resticPath string) (*RepoOrchestrator, error) { + if config.Instance == "" { + return nil, errors.New("instance is a required field in the backrest config") + } + + var opts []restic.GenericOption + if p := repoConfig.GetPassword(); p != "" { + opts = append(opts, restic.WithEnv("RESTIC_PASSWORD="+p)) + } + + opts = append(opts, restic.WithEnviron()) + + if env := repoConfig.GetEnv(); len(env) != 0 { + for _, e := range env { + opts = append(opts, restic.WithEnv(ExpandEnv(e))) + } + } + + for _, f := range repoConfig.GetFlags() { + args, err := shlex.Split(ExpandEnv(f)) + if err != nil { + return nil, fmt.Errorf("parse flag %q for repo %q: %w", f, repoConfig.Id, err) + } + opts = append(opts, restic.WithFlags(args...)) + } + + // Resolve command prefix + if extraOpts, err := resolveCommandPrefix(repoConfig.GetCommandPrefix()); err != nil { + return nil, fmt.Errorf("resolve command prefix: %w", err) + } else { + opts = append(opts, extraOpts...) + } + + // Add BatchMode=yes to sftp.args if it's not already set. + if slices.IndexFunc(repoConfig.GetFlags(), func(a string) bool { + return strings.Contains(a, "sftp.args") + }) == -1 { + opts = append(opts, restic.WithFlags("-o", "sftp.args=-oBatchMode=yes")) + } + + repo := restic.NewRepo(resticPath, repoConfig.GetUri(), opts...) + + return &RepoOrchestrator{ + config: config, + repoConfig: repoConfig, + repo: repo, + }, nil +} + +func (r *RepoOrchestrator) logger(ctx context.Context) *zap.Logger { + return logging.Logger(ctx, "[repo-manager] ").With(zap.String("repo", r.repoConfig.Id)) +} + +func (r *RepoOrchestrator) Exists(ctx context.Context) error { + return r.repo.Exists(ctx) +} + +func (r *RepoOrchestrator) Init(ctx context.Context) error { + ctx, flush := forwardResticLogs(ctx) + defer flush() + + return r.repo.Init(ctx) +} + +func (r *RepoOrchestrator) Snapshots(ctx context.Context) ([]*restic.Snapshot, error) { + ctx, flush := forwardResticLogs(ctx) + defer flush() + + snapshots, err := r.repo.Snapshots(ctx) + if err != nil { + return nil, fmt.Errorf("get snapshots for repo %v: %w", r.repoConfig.Id, err) + } + sortSnapshotsByTime(snapshots) + return snapshots, nil +} + +func (r *RepoOrchestrator) SnapshotsForPlan(ctx context.Context, plan *v1.Plan) ([]*restic.Snapshot, error) { + ctx, flush := forwardResticLogs(ctx) + defer flush() + + tags := []string{TagForPlan(plan.Id)} + if r.config.Instance != "" { + tags = append(tags, TagForInstance(r.config.Instance)) + } + + snapshots, err := r.repo.Snapshots(ctx, restic.WithFlags("--tag", strings.Join(tags, ","))) + if err != nil { + return nil, fmt.Errorf("get snapshots for plan %q: %w", plan.Id, err) + } + sortSnapshotsByTime(snapshots) + return snapshots, nil +} + +func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCallback func(event *restic.BackupProgressEntry)) (*restic.BackupProgressEntry, error) { + l := r.logger(ctx) + l.Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id)) + + r.mu.Lock() + defer r.mu.Unlock() + + snapshots, err := r.SnapshotsForPlan(ctx, plan) + if err != nil { + return nil, fmt.Errorf("failed to get snapshots for plan: %w", err) + } + + l.Debug("got snapshots for plan", zap.String("repo", r.repoConfig.Id), zap.Int("count", len(snapshots)), zap.String("plan", plan.Id), zap.String("tag", TagForPlan(plan.Id))) + + startTime := time.Now() + + var opts []restic.GenericOption + opts = append(opts, restic.WithFlags( + "--exclude-caches", + "--tag", TagForPlan(plan.Id), + )) + + if r.config.Instance != "" { + opts = append(opts, restic.WithFlags("--tag", TagForInstance(r.config.Instance))) + } else { + zap.L().Warn("Creating a backup without an 'instance' tag as no value is set in the config. In a future backrest release this will be an error.") + } + + for _, exclude := range plan.Excludes { + opts = append(opts, restic.WithFlags("--exclude", exclude)) + } + for _, iexclude := range plan.Iexcludes { + opts = append(opts, restic.WithFlags("--iexclude", iexclude)) + } + if len(snapshots) > 0 { + opts = append(opts, restic.WithFlags("--parent", snapshots[len(snapshots)-1].Id)) + } + + for _, f := range plan.GetBackupFlags() { + args, err := shlex.Split(f) + if err != nil { + return nil, fmt.Errorf("failed to parse backup flag %q for plan %q: %w", f, plan.Id, err) + } + opts = append(opts, restic.WithFlags(args...)) + } + + ctx, flush := forwardResticLogs(ctx) + defer flush() + l.Debug("starting backup", zap.String("plan", plan.Id)) + summary, err := r.repo.Backup(ctx, plan.Paths, progressCallback, opts...) + if err != nil { + return summary, fmt.Errorf("failed to backup: %w", err) + } + + l.Debug("backup completed", zap.Duration("duration", time.Since(startTime))) + return summary, nil +} + +func (r *RepoOrchestrator) ListSnapshotFiles(ctx context.Context, snapshotId string, path string) ([]*v1.LsEntry, error) { + ctx, flush := forwardResticLogs(ctx) + defer flush() + + _, entries, err := r.repo.ListDirectory(ctx, snapshotId, path) + if err != nil { + return nil, fmt.Errorf("failed to list snapshot files: %w", err) + } + + lsEnts := make([]*v1.LsEntry, 0, len(entries)) + for _, entry := range entries { + lsEnts = append(lsEnts, entry.ToProto()) + } + + return lsEnts, nil +} + +func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan, tags []string) ([]*v1.ResticSnapshot, error) { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + policy := plan.Retention + if policy == nil { + return nil, fmt.Errorf("plan %q has no retention policy", plan.Id) + } + + result, err := r.repo.Forget( + ctx, protoutil.RetentionPolicyFromProto(plan.Retention), + restic.WithFlags("--tag", strings.Join(tags, ",")), + restic.WithFlags("--group-by", ""), + ) + if err != nil { + return nil, fmt.Errorf("get snapshots for repo %v: %w", r.repoConfig.Id, err) + } + + var forgotten []*v1.ResticSnapshot + for _, snapshot := range result.Remove { + snapshotProto := protoutil.SnapshotToProto(&snapshot) + if err := protoutil.ValidateSnapshot(snapshotProto); err != nil { + return nil, fmt.Errorf("snapshot validation failed: %w", err) + } + forgotten = append(forgotten, snapshotProto) + } + + r.logger(ctx).Debug("forget snapshots", zap.String("plan", plan.Id), zap.Int("count", len(forgotten)), zap.Any("policy", policy)) + + return forgotten, nil +} + +func (r *RepoOrchestrator) ForgetSnapshot(ctx context.Context, snapshotId string) error { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + r.logger(ctx).Debug("forget snapshot with ID", zap.String("snapshot", snapshotId), zap.String("repo", r.repoConfig.Id)) + return r.repo.ForgetSnapshot(ctx, snapshotId) +} + +func (r *RepoOrchestrator) Prune(ctx context.Context, output io.Writer) error { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + policy := r.repoConfig.PrunePolicy + if policy == nil { + policy = &v1.PrunePolicy{ + MaxUnusedPercent: 25, + } + } + + var opts []restic.GenericOption + if policy.MaxUnusedBytes != 0 { + opts = append(opts, restic.WithFlags("--max-unused", fmt.Sprintf("%vB", policy.MaxUnusedBytes))) + } else if policy.MaxUnusedPercent != 0 { + opts = append(opts, restic.WithFlags("--max-unused", fmt.Sprintf("%v%%", policy.MaxUnusedPercent))) + } + + r.logger(ctx).Debug("prune snapshots") + err := r.repo.Prune(ctx, output, opts...) + if err != nil { + return fmt.Errorf("prune snapshots for repo %v: %w", r.repoConfig.Id, err) + } + return nil +} + +func (r *RepoOrchestrator) Check(ctx context.Context, output io.Writer) error { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + var opts []restic.GenericOption + if r.repoConfig.CheckPolicy != nil { + switch m := r.repoConfig.CheckPolicy.Mode.(type) { + case *v1.CheckPolicy_ReadDataSubsetPercent: + if m.ReadDataSubsetPercent > 0 { + opts = append(opts, restic.WithFlags(fmt.Sprintf("--read-data-subset=%.4f%%", m.ReadDataSubsetPercent))) + } + case *v1.CheckPolicy_StructureOnly: + default: + } + } + + r.logger(ctx).Debug("checking repo") + err := r.repo.Check(ctx, output, opts...) + if err != nil { + return fmt.Errorf("check repo %v: %w", r.repoConfig.Id, err) + } + return nil +} + +func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, snapshotPath string, target string, progressCallback func(event *v1.RestoreProgressEntry)) (*v1.RestoreProgressEntry, error) { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + r.logger(ctx).Debug("restore snapshot", zap.String("snapshot", snapshotId), zap.String("target", target)) + + var opts []restic.GenericOption + opts = append(opts, restic.WithFlags("--target", target)) + + if snapshotPath != "" { + normalizedPath := strings.ReplaceAll(snapshotPath, "\\", "/") + + dir := path.Dir(normalizedPath) + base := path.Base(normalizedPath) + + if dir != "" { + snapshotId = snapshotId + ":" + dir + } + if base != "" { + opts = append(opts, restic.WithFlags("--include", escapeGlob(base))) + } + } + + summary, err := r.repo.Restore(ctx, snapshotId, func(event *restic.RestoreProgressEntry) { + if progressCallback != nil { + progressCallback(protoutil.RestoreProgressEntryToProto(event)) + } + }, opts...) + if err != nil { + return nil, fmt.Errorf("restore snapshot %q for repo %v: %w", snapshotId, r.repoConfig.Id, err) + } + + return protoutil.RestoreProgressEntryToProto(summary), nil +} + +// UnlockIfAutoEnabled unlocks the repo if the auto unlock feature is enabled. +func (r *RepoOrchestrator) UnlockIfAutoEnabled(ctx context.Context) error { + if !r.repoConfig.AutoUnlock { + return nil + } + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + r.logger(ctx).Debug("auto-unlocking repo", zap.String("repo", r.repoConfig.Id)) + + return r.repo.Unlock(ctx) +} + +func (r *RepoOrchestrator) Unlock(ctx context.Context) error { + r.mu.Lock() + defer r.mu.Unlock() + + r.logger(ctx).Debug("unlocking repo", zap.String("repo", r.repoConfig.Id)) + r.repo.Unlock(ctx) + + return nil +} + +func (r *RepoOrchestrator) Stats(ctx context.Context) (*v1.RepoStats, error) { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + r.logger(ctx).Debug("getting repo stats", zap.String("repo", r.repoConfig.Id)) + stats, err := r.repo.Stats(ctx) + if err != nil { + return nil, fmt.Errorf("stats for repo %v: %w", r.repoConfig.Id, err) + } + + return protoutil.RepoStatsToProto(stats), nil +} + +func (r *RepoOrchestrator) AddTags(ctx context.Context, snapshotIDs []string, tags []string) error { + r.mu.Lock() + defer r.mu.Unlock() + ctx, flush := forwardResticLogs(ctx) + defer flush() + + for idx, snapshotIDs := range chunkBy(snapshotIDs, 20) { + r.logger(ctx).Debug("adding tag to snapshots", zap.Strings("snapshots", snapshotIDs), zap.Strings("tags", tags)) + if err := r.repo.AddTags(ctx, snapshotIDs, tags); err != nil { + return fmt.Errorf("batch %v: %w", idx, err) + } + } + + return nil +} + +// RunCommand runs a command in the repo's environment. +// NOTE: this function does not lock the repo. +func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, writer io.Writer) error { + ctx, flush := forwardResticLogs(ctx) + defer flush() + + r.logger(ctx).Debug("running command", zap.String("command", command)) + args, err := shlex.Split(command) + if err != nil { + return fmt.Errorf("parse command: %w", err) + } + + ctx = restic.ContextWithLogger(ctx, writer) + return r.repo.GenericCommand(ctx, args) +} + +func (r *RepoOrchestrator) Config() *v1.Repo { + if r == nil { + return nil + } + return r.repoConfig +} + +func (r *RepoOrchestrator) RepoGUID() (string, error) { + r.mu.Lock() + defer r.mu.Unlock() + cfg, err := r.repo.Config(context.Background()) + return cryptoutil.TruncateID(cfg.Id, cryptoutil.DefaultIDBits), err +} + +func sortSnapshotsByTime(snapshots []*restic.Snapshot) { + sort.SliceStable(snapshots, func(i, j int) bool { + return snapshots[i].UnixTimeMs() < snapshots[j].UnixTimeMs() + }) +} + +func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) { + for chunkSize < len(items) { + items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize]) + } + return append(chunks, items) +} + +var globEscapeReplacer = strings.NewReplacer(`\`, `\\`, `*`, `\*`, `?`, `\?`, `[`, `\[`, `]`, `\]`) + +func escapeGlob(s string) string { + if runtime.GOOS == "windows" { + return s // escaping is not supported on Windows + } + return globEscapeReplacer.Replace(s) +} diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go new file mode 100644 index 000000000..c27c11c7f --- /dev/null +++ b/internal/orchestrator/repo/repo_test.go @@ -0,0 +1,371 @@ +package repo + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "path" + "runtime" + "slices" + "strings" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/test/helpers" + test "github.com/garethgeorge/backrest/test/helpers" + "golang.org/x/sync/errgroup" +) + +var configForTest = &v1.Config{ + Instance: "test", +} + +func TestBackup(t *testing.T) { + t.Parallel() + + testData := test.CreateTestData(t) + + tcs := []struct { + name string + repo *v1.Repo + plan *v1.Plan + excludeGoos []string + }{ + { + name: "backup", + repo: &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + }, + plan: &v1.Plan{ + Id: "test", + Repo: "test", + Paths: []string{testData}, + }, + }, + { + name: "backup with ionice", + repo: &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + CommandPrefix: &v1.CommandPrefix{ + IoNice: v1.CommandPrefix_IO_BEST_EFFORT_LOW, + CpuNice: v1.CommandPrefix_CPU_LOW, + }, + }, + plan: &v1.Plan{ + Id: "test", + Repo: "test", + Paths: []string{testData}, + }, + excludeGoos: []string{"windows", "darwin"}, + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if slices.Contains(tc.excludeGoos, runtime.GOOS) { + t.Skipf("skipping test on %s", runtime.GOOS) + } + + orchestrator := initRepoHelper(t, configForTest, tc.repo) + + summary, err := orchestrator.Backup(context.Background(), tc.plan, nil) + if err != nil { + t.Fatalf("backup error: %v", err) + } + + if summary.SnapshotId == "" { + t.Fatal("expected snapshot id") + } + + if summary.FilesNew != 100 { + t.Fatalf("expected 100 new files, got %d", summary.FilesNew) + } + }) + } +} + +func TestRestore(t *testing.T) { + t.Parallel() + + // Use a filepath that exercises a few of the glob characters to test escaping + messyFilePathToTestGlobs := "test.txt" + if runtime.GOOS != "windows" { + messyFilePathToTestGlobs = "test*?[].txt" + } + + testFile := path.Join(t.TempDir(), messyFilePathToTestGlobs) + if err := ioutil.WriteFile(testFile, []byte("lorum ipsum"), 0644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + r := &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + } + + plan := &v1.Plan{ + Id: "test", + Repo: "test", + Paths: []string{testFile}, + } + + orchestrator := initRepoHelper(t, configForTest, r) + + // Create a backup of the single file + summary, err := orchestrator.Backup(context.Background(), plan, nil) + if err != nil { + t.Fatalf("backup error: %v", err) + } + if summary.SnapshotId == "" { + t.Fatal("expected snapshot id") + } + if summary.FilesNew != 1 { + t.Fatalf("expected 1 new file, got %d", summary.FilesNew) + } + + // Restore the file + restoreDir := t.TempDir() + snapshotPath := strings.ReplaceAll(testFile, ":", "") // remove the colon from the windows path e.g. C:\test.txt -> C\test.txt + restoreSummary, err := orchestrator.Restore(context.Background(), summary.SnapshotId, snapshotPath, restoreDir, nil) + if err != nil { + t.Fatalf("restore error: %v", err) + } + t.Logf("restore summary: %+v", restoreSummary) + + if runtime.GOOS == "windows" { + return + } + + if restoreSummary.FilesRestored != 1 { + t.Errorf("expected 1 new file, got %d", restoreSummary.FilesRestored) + } + if restoreSummary.TotalFiles != 1 { + t.Errorf("expected 1 total file, got %d", restoreSummary.TotalFiles) + } + + // Check the restored file + restoredFile := path.Join(restoreDir, messyFilePathToTestGlobs) + if _, err := os.Stat(restoredFile); err != nil { + t.Fatalf("failed to stat restored file: %v", err) + } + restoredData, err := os.ReadFile(restoredFile) + if err != nil { + t.Fatalf("failed to read restored file: %v", err) + } + if string(restoredData) != "lorum ipsum" { + t.Fatalf("expected 'test', got '%s'", restoredData) + } +} + +func TestSnapshotParenting(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + testData := test.CreateTestData(t) + + // create a new repo with cache disabled for testing + r := &v1.Repo{ + Id: "test", + Uri: repo, + Password: "test", + Flags: []string{"--no-cache"}, + } + + plans := []*v1.Plan{ + { + Id: "test", + Repo: "test", + Paths: []string{testData}, + }, + { + Id: "test2", + Repo: "test", + Paths: []string{testData}, + }, + } + + orchestrator := initRepoHelper(t, configForTest, r) + + var eg errgroup.Group + for _, plan := range plans { + eg.Go(func() error { + for i := 0; i < 2; i++ { + summary, err := orchestrator.Backup(context.Background(), plan, nil) + if err != nil { + return fmt.Errorf("failed to backup plan %s: %v", plan.Id, err) + } + + if summary.SnapshotId == "" { + return errors.New("expected snapshot id") + } + + if summary.TotalFilesProcessed != 100 { + t.Logf("summary is: %+v", summary) + return fmt.Errorf("expected 100 done files, got %d", summary.TotalFilesProcessed) + } + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + for _, plan := range plans { + t.Run("verify_"+plan.Id, func(t *testing.T) { + t.Parallel() + snapshots, err := orchestrator.SnapshotsForPlan(context.Background(), plan) + if err != nil { + t.Errorf("failed to get snapshots for plan %s: %v", plan.Id, err) + return + } + + if len(snapshots) != 2 { + t.Errorf("expected 4 snapshots, got %d", len(snapshots)) + } + + for i := 1; i < len(snapshots); i++ { + prev := snapshots[i-1] + curr := snapshots[i] + + if prev.UnixTimeMs() >= curr.UnixTimeMs() { + t.Errorf("snapshots are out of order") + } + + if prev.Id != curr.Parent { + t.Errorf("expected snapshot %s to have parent %s, got %s", curr.Id, prev.Id, curr.Parent) + } + + if !slices.Contains(curr.Tags, TagForPlan(plan.Id)) { + t.Errorf("expected snapshot %s to have tag %s", curr.Id, TagForPlan(plan.Id)) + } + } + }) + } + + t.Run("verify_snapshot_count", func(t *testing.T) { + t.Parallel() + snapshots, err := orchestrator.Snapshots(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(snapshots) != 4 { + t.Errorf("expected 4 snapshots, got %d", len(snapshots)) + } + }) +} + +func TestEnvVarPropagation(t *testing.T) { + repo := t.TempDir() + + // create a new repo with cache disabled for testing + r := &v1.Repo{ + Id: "test", + Uri: repo, + Flags: []string{"--no-cache"}, + Env: []string{"RESTIC_PASSWORD=${MY_FOO}"}, + } + + orchestrator, err := NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) + if err != nil { + t.Fatalf("failed to create repo orchestrator: %v", err) + } + + err = orchestrator.Init(context.Background()) + if err == nil || !strings.Contains(err.Error(), "password") { + t.Fatalf("expected error about RESTIC_PASSWORD, got: %v", err) + } + + // set the env var + os.Setenv("MY_FOO", "bar") + defer os.Unsetenv("MY_FOO") + orchestrator, err = NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) + if err != nil { + t.Fatalf("failed to create repo orchestrator: %v", err) + } + + err = orchestrator.Init(context.Background()) + if err != nil { + t.Fatalf("backup error: %v", err) + } +} + +func TestCheck(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + repo *v1.Repo + }{ + { + name: "check structure", + repo: &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + CheckPolicy: &v1.CheckPolicy{ + Mode: nil, + }, + }, + }, + { + name: "read data percent", + repo: &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + CheckPolicy: &v1.CheckPolicy{ + Mode: &v1.CheckPolicy_ReadDataSubsetPercent{ + ReadDataSubsetPercent: 50, + }, + }, + }, + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + orchestrator := initRepoHelper(t, configForTest, tc.repo) + buf := bytes.NewBuffer(nil) + + err := orchestrator.Init(context.Background()) + if err != nil { + t.Fatalf("init error: %v", err) + } + + err = orchestrator.Check(context.Background(), buf) + if err != nil { + t.Errorf("check error: %v", err) + } + t.Logf("check output: %s", buf.String()) + }) + } +} + +func initRepoHelper(t *testing.T, config *v1.Config, repo *v1.Repo) *RepoOrchestrator { + orchestrator, err := NewRepoOrchestrator(config, repo, helpers.ResticBinary(t)) + if err != nil { + t.Fatalf("failed to create repo orchestrator: %v", err) + } + + err = orchestrator.Init(context.Background()) + if err != nil { + t.Fatalf("init error: %v", err) + } + + return orchestrator +} diff --git a/internal/orchestrator/repo/tags.go b/internal/orchestrator/repo/tags.go new file mode 100644 index 000000000..1d740be09 --- /dev/null +++ b/internal/orchestrator/repo/tags.go @@ -0,0 +1,36 @@ +package repo + +import ( + "fmt" + "strings" +) + +// TagForPlan returns a tag for the plan. +func TagForPlan(planId string) string { + return fmt.Sprintf("plan:%s", planId) +} + +// TagForInstance returns a tag for the instance. +func TagForInstance(instanceId string) string { + return fmt.Sprintf("created-by:%s", instanceId) +} + +// InstanceIDFromTags returns the instance ID from the tags, or an empty string if not found. +func InstanceIDFromTags(tags []string) string { + for _, tag := range tags { + if strings.HasPrefix(tag, "created-by:") { + return tag[len("created-by:"):] + } + } + return "" +} + +// PlanFromTags returns the plan ID from the tags, or an empty string if not found. +func PlanFromTags(tags []string) string { + for _, tag := range tags { + if strings.HasPrefix(tag, "plan:") { + return tag[len("plan:"):] + } + } + return "" +} diff --git a/internal/orchestrator/scheduling_test.go b/internal/orchestrator/scheduling_test.go new file mode 100644 index 000000000..7db0cf1a7 --- /dev/null +++ b/internal/orchestrator/scheduling_test.go @@ -0,0 +1,265 @@ +package orchestrator + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" +) + +type testTask struct { + tasks.BaseTask + onRun func() error + onNext func(curTime time.Time) *time.Time +} + +var _ tasks.Task = &testTask{} + +func newTestTask(onRun func() error, onNext func(curTime time.Time) *time.Time) tasks.Task { + return &testTask{ + BaseTask: tasks.BaseTask{ + TaskName: "test task", + TaskRepo: &v1.Repo{Id: "repo", Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits)}, + TaskPlanID: "plan", + }, + onRun: onRun, + onNext: onNext, + } +} + +func (t *testTask) Next(curTime time.Time, runner tasks.TaskRunner) (tasks.ScheduledTask, error) { + at := t.onNext(curTime) + if at == nil { + return tasks.NeverScheduledTask, nil + } + return tasks.ScheduledTask{ + Task: t, + RunAt: *at, + }, nil +} + +func (t *testTask) Run(ctx context.Context, st tasks.ScheduledTask, runner tasks.TaskRunner) error { + return t.onRun() +} + +func newTestOrchestrator(t *testing.T) *Orchestrator { + cfgMgr := &config.ConfigManager{ + Store: &config.MemoryStore{ + Config: config.NewDefaultConfig(), + }, + } + orch, err := NewOrchestrator("", cfgMgr, nil, nil) + if err != nil { + t.Fatalf("failed to create orchestrator: %v", err) + } + return orch +} + +func TestTaskScheduling(t *testing.T) { + t.Parallel() + + // Arrange + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + orch := newTestOrchestrator(t) + + var wg sync.WaitGroup + wg.Add(1) + task := newTestTask( + func() error { + wg.Done() + cancel() + return nil + }, + func(t time.Time) *time.Time { + t = t.Add(10 * time.Millisecond) + return &t + }, + ) + + wg.Add(1) + go func() { + defer wg.Done() + orch.Run(ctx) + }() + + // Act + orch.ScheduleTask(task, tasks.TaskPriorityDefault) + + // Assert passes if all tasks run and the orchestrator exists when cancelled. + wg.Wait() +} + +func TestTaskRescheduling(t *testing.T) { + t.Parallel() + + // Arrange + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + orch := newTestOrchestrator(t) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + orch.Run(ctx) + }() + + // Act + count := 0 + ranTimes := 0 + + orch.ScheduleTask(newTestTask( + func() error { + ranTimes += 1 + if ranTimes == 10 { + cancel() + } + return nil + }, + func(t time.Time) *time.Time { + if count < 10 { + count += 1 + return &t + } + return nil + }, + ), tasks.TaskPriorityDefault) + + wg.Wait() + + if count != 10 { + t.Errorf("expected 10 Next calls, got %d", count) + } + + if ranTimes != 10 { + t.Errorf("expected 10 Run calls, got %d", ranTimes) + } +} + +func TestTaskRetry(t *testing.T) { + t.Parallel() + + // Arrange + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + orch := newTestOrchestrator(t) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + orch.Run(ctx) + }() + + // Act + count := 0 + ranTimes := 0 + + orch.ScheduleTask(newTestTask( + func() error { + ranTimes += 1 + if ranTimes == 10 { + cancel() + } + return &tasks.TaskRetryError{ + Err: errors.New("retry please"), + Backoff: func(attempt int) time.Duration { return 0 }, + } + }, + func(t time.Time) *time.Time { + count += 1 + return &t + }, + ), tasks.TaskPriorityDefault) + + wg.Wait() + + if count != 1 { + t.Errorf("expected 1 Next calls because this test covers retries, got %d", count) + } + + if ranTimes != 10 { + t.Errorf("expected 10 Run calls, got %d", ranTimes) + } +} + +func TestGracefulShutdown(t *testing.T) { + t.Parallel() + + // Arrange + orch := newTestOrchestrator(t) + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + + // Act + orch.Run(ctx) +} + +func TestSchedulerWait(t *testing.T) { + t.Parallel() + + // Arrange + orch := newTestOrchestrator(t) + orch.taskQueue.Reset() + + ran := make(chan struct{}) + didRun := false + orch.ScheduleTask(newTestTask( + func() error { + close(ran) + return nil + }, + func(t time.Time) *time.Time { + if didRun { + return nil + } + t = t.Add(100 * time.Millisecond) + didRun = true + return &t + }, + ), tasks.TaskPriorityDefault) + + // Act + go orch.Run(context.Background()) + + // Assert + select { + case <-time.NewTimer(20 * time.Millisecond).C: + case <-ran: + t.Errorf("expected task to not run yet") + } + + // Schedule another task just to trigger a queue refresh + orch.ScheduleTask(&testTask{ + onNext: func(t time.Time) *time.Time { + t = t.Add(1000 * time.Second) + return &t + }, + onRun: func() error { + t.Fatalf("should never run") + return nil + }, + }, tasks.TaskPriorityDefault) + + select { + case <-time.NewTimer(200 * time.Millisecond).C: + t.Errorf("expected task to run") + case <-ran: + } +} diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go new file mode 100644 index 000000000..202360a01 --- /dev/null +++ b/internal/orchestrator/taskrunnerimpl.go @@ -0,0 +1,178 @@ +package orchestrator + +import ( + "context" + "errors" + "fmt" + "io" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// taskRunnerImpl is an implementation of TaskRunner for the default orchestrator. +type taskRunnerImpl struct { + orchestrator *Orchestrator + t tasks.Task + op *v1.Operation + plan *v1.Plan // cache, populated on first call to Plan() + config *v1.Config // cache, populated on first call to Config() +} + +var _ tasks.TaskRunner = &taskRunnerImpl{} + +func newTaskRunnerImpl(orchestrator *Orchestrator, task tasks.Task, op *v1.Operation) *taskRunnerImpl { + return &taskRunnerImpl{ + config: orchestrator.config, + orchestrator: orchestrator, + t: task, + op: op, + } +} + +func (t *taskRunnerImpl) findPlan() (*v1.Plan, error) { + if t.plan != nil { + return t.plan, nil + } + var err error + t.plan, err = t.orchestrator.GetPlan(t.t.PlanID()) + return t.plan, err +} + +func (t *taskRunnerImpl) InstanceID() string { + return t.config.Instance +} + +func (t *taskRunnerImpl) GetOperation(id int64) (*v1.Operation, error) { + return t.orchestrator.OpLog.Get(id) +} + +func (t *taskRunnerImpl) CreateOperation(op ...*v1.Operation) error { + for _, o := range op { + if o.InstanceId != "" { + continue + } + o.InstanceId = t.InstanceID() + } + return t.orchestrator.OpLog.Add(op...) +} + +func (t *taskRunnerImpl) UpdateOperation(op ...*v1.Operation) error { + for _, o := range op { + if o.InstanceId != "" { + continue + } + o.InstanceId = t.InstanceID() + } + return t.orchestrator.OpLog.Update(op...) +} + +func (t *taskRunnerImpl) DeleteOperation(id ...int64) error { + return t.orchestrator.OpLog.Delete(id...) +} + +func (t *taskRunnerImpl) Orchestrator() *Orchestrator { + return t.orchestrator +} + +func (t *taskRunnerImpl) QueryOperations(q oplog.Query, fn func(*v1.Operation) error) error { + return t.orchestrator.OpLog.Query(q, fn) +} + +func (t *taskRunnerImpl) ExecuteHooks(ctx context.Context, events []v1.Hook_Condition, vars tasks.HookVars) error { + vars.Task = t.t.Name() + if t.op != nil { + vars.Duration = time.Since(time.UnixMilli(t.op.UnixTimeStartMs)) + } + + vars.CurTime = time.Now() + + repoID := t.t.RepoID() + planID := t.t.PlanID() + var plan *v1.Plan + if repo := t.t.Repo(); repo != nil { + vars.Repo = repo + } + if planID != "" { + plan, _ = t.findPlan() + vars.Plan = plan + } + if vars.Plan == nil { + vars.Plan = &v1.Plan{ + Id: t.t.PlanID(), // make a fake plan that conveys only the ID, e.g. for unassociated operations OR system plan. + } + } + + hookTasks, err := hook.TasksTriggeredByEvent(t.Config(), repoID, planID, t.op, events, vars) + if err != nil { + return err + } + + for _, task := range hookTasks { + st, err := t.orchestrator.CreateUnscheduledTask(task, tasks.TaskPriorityDefault, time.Now()) + if err != nil { + return fmt.Errorf("creating task for hook: %w", err) + } + if err := t.orchestrator.RunTask(ctx, st); hook.IsHaltingError(err) { + var cancelErr *hook.HookErrorRequestCancel + var retryErr *hook.HookErrorRetry + if errors.As(err, &cancelErr) { + return fmt.Errorf("%v: %w: %w", task.Name(), &tasks.TaskCancelledError{}, cancelErr.Err) + } else if errors.As(err, &retryErr) { + return fmt.Errorf("%v: %w", task.Name(), &tasks.TaskRetryError{ + Err: retryErr.Err, + Backoff: retryErr.Backoff, + }) + } + return fmt.Errorf("%v: %w", task.Name(), err) + } + } + return nil +} + +func (t *taskRunnerImpl) GetRepo(repoID string) (*v1.Repo, error) { + if repoID == t.t.RepoID() { + return t.t.Repo(), nil + } + return t.orchestrator.GetRepo(repoID) +} + +func (t *taskRunnerImpl) GetPlan(planID string) (*v1.Plan, error) { + if planID == t.t.PlanID() { + return t.findPlan() // optimization for the common case of the current plan + } + return t.orchestrator.GetPlan(planID) +} + +func (t *taskRunnerImpl) GetRepoOrchestrator(repoID string) (*repo.RepoOrchestrator, error) { + return t.orchestrator.GetRepoOrchestrator(repoID) +} + +func (t *taskRunnerImpl) ScheduleTask(task tasks.Task, priority int) error { + return t.orchestrator.ScheduleTask(task, priority) +} + +func (t *taskRunnerImpl) Config() *v1.Config { + if t.config != nil { + return t.config + } + t.config = t.orchestrator.Config() + return t.config +} + +func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { + return logging.Logger(ctx, "[tasklog] ").Named(t.t.Name()) +} + +func (t *taskRunnerImpl) LogrefWriter() (string, io.WriteCloser, error) { + logID := uuid.New().String() + writer, err := t.orchestrator.logStore.Create(logID, t.op.GetId(), time.Duration(0)) + return logID, writer, err +} diff --git a/internal/orchestrator/tasks/errors.go b/internal/orchestrator/tasks/errors.go new file mode 100644 index 000000000..5da3ef496 --- /dev/null +++ b/internal/orchestrator/tasks/errors.go @@ -0,0 +1,35 @@ +package tasks + +import ( + "fmt" + "time" +) + +// TaskCancelledError is returned when a task is cancelled. +type TaskCancelledError struct { +} + +func (e TaskCancelledError) Error() string { + return "task cancelled" +} + +func (e TaskCancelledError) Is(err error) bool { + _, ok := err.(TaskCancelledError) + return ok +} + +type RetryBackoffPolicy = func(attempt int) time.Duration + +// TaskRetryError is returned when a task should be retried after a specified backoff duration. +type TaskRetryError struct { + Err error + Backoff RetryBackoffPolicy +} + +func (e TaskRetryError) Error() string { + return fmt.Sprintf("retry: %v", e.Err.Error()) +} + +func (e TaskRetryError) Unwrap() error { + return e.Err +} diff --git a/internal/orchestrator/tasks/flowidutil.go b/internal/orchestrator/tasks/flowidutil.go new file mode 100644 index 000000000..bd0ddbbf3 --- /dev/null +++ b/internal/orchestrator/tasks/flowidutil.go @@ -0,0 +1,32 @@ +package tasks + +import ( + "fmt" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" +) + +// FlowIDForSnapshotID returns the flow ID associated with the backup task that created snapshot ID or 0 if not found. +func FlowIDForSnapshotID(runner TaskRunner, repoGUID string, snapshotID string) (int64, error) { + var flowID int64 + if err := runner.QueryOperations(oplog.Query{SnapshotID: &snapshotID}, func(op *v1.Operation) error { + if op.RepoGuid != repoGUID { + // ignore operations from other repos, done here instead of in the query + // to encourage sqlite to make the right index choice. SnapshotID is vastly + // more selective than RepoGUID. + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationBackup); !ok { + return nil + } + if flowID != 0 { + return fmt.Errorf("multiple flow IDs found for snapshot %q", snapshotID) + } + flowID = op.FlowId + return nil + }); err != nil { + return 0, fmt.Errorf("get flow id for snapshot %q : %w", snapshotID, err) + } + return flowID, nil +} diff --git a/internal/orchestrator/tasks/hookvars.go b/internal/orchestrator/tasks/hookvars.go new file mode 100644 index 000000000..2366ae5b4 --- /dev/null +++ b/internal/orchestrator/tasks/hookvars.go @@ -0,0 +1,195 @@ +package tasks + +import ( + "bytes" + "encoding/json" + "fmt" + "text/template" + "time" + + "al.essio.dev/pkg/shellescape" + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/pkg/restic" +) + +// HookVars is the set of variables that are available to a hook. Some of these are optional. +// NOTE: names of HookVars may change between versions of backrest. This is not a guaranteed stable API. +// when names change hooks will require updating. +type HookVars struct { + Task string // the name of the task that triggered the hook. + Event v1.Hook_Condition // the event that triggered the hook. + Repo *v1.Repo // the v1.Repo that triggered the hook. + Plan *v1.Plan // the v1.Plan that triggered the hook. + SnapshotId string // the snapshot ID that triggered the hook. + SnapshotStats *restic.BackupProgressEntry // the summary of the backup operation. + CurTime time.Time // the current time as time.Time + Duration time.Duration // the duration of the operation that triggered the hook. + Error string // the error that caused the hook to run as a string. +} + +func (v HookVars) EventName(cond v1.Hook_Condition) string { + switch cond { + case v1.Hook_CONDITION_SNAPSHOT_START: + return "snapshot start" + case v1.Hook_CONDITION_SNAPSHOT_END: + return "snapshot end" + case v1.Hook_CONDITION_ANY_ERROR: + return "error" + case v1.Hook_CONDITION_SNAPSHOT_ERROR: + return "snapshot error" + case v1.Hook_CONDITION_SNAPSHOT_WARNING: + return "snapshot warning" + case v1.Hook_CONDITION_SNAPSHOT_SUCCESS: + return "snapshot success" + case v1.Hook_CONDITION_SNAPSHOT_SKIPPED: + return "snapshot skipped" + case v1.Hook_CONDITION_CHECK_START: + return "check start" + case v1.Hook_CONDITION_CHECK_ERROR: + return "check error" + case v1.Hook_CONDITION_CHECK_SUCCESS: + return "check success" + case v1.Hook_CONDITION_PRUNE_START: + return "prune start" + case v1.Hook_CONDITION_PRUNE_ERROR: + return "prune error" + case v1.Hook_CONDITION_PRUNE_SUCCESS: + return "prune success" + case v1.Hook_CONDITION_FORGET_START: + return "forget start" + case v1.Hook_CONDITION_FORGET_ERROR: + return "forget error" + case v1.Hook_CONDITION_FORGET_SUCCESS: + return "forget success" + default: + return "unknown" + } +} + +func (v HookVars) FormatTime(t time.Time) string { + return t.Format(time.RFC3339) +} + +func (v HookVars) FormatDuration(d time.Duration) string { + return d.Truncate(time.Millisecond).String() +} + +func (v HookVars) number(n any) int { + switch n := n.(type) { + case int: + return n + case int32: + return int(n) + case int64: + return int(n) + default: + return 0 + } +} + +func (v HookVars) FormatSizeBytes(val any) string { + size := v.number(val) + sizes := []string{"B", "KB", "MB", "GB", "TB", "PB"} + i := 0 + prev := size + for size > 1000 { + size /= 1000 + prev = size + i++ + } + return fmt.Sprintf("%d.%03d %s", size, prev, sizes[i]) +} + +func (v HookVars) IsError(cond v1.Hook_Condition) bool { + return cond == v1.Hook_CONDITION_ANY_ERROR || cond == v1.Hook_CONDITION_SNAPSHOT_ERROR +} + +func (v HookVars) ShellEscape(s string) string { + return shellescape.Quote(s) +} + +func (v HookVars) JsonMarshal(s any) string { + b, err := json.Marshal(s) + if err != nil { + return "" + } + return string(b) +} + +func (v HookVars) Summary() (string, error) { + switch v.Event { + case v1.Hook_CONDITION_SNAPSHOT_START: + return v.renderTemplate(templateForSnapshotStart) + case v1.Hook_CONDITION_SNAPSHOT_END, v1.Hook_CONDITION_SNAPSHOT_WARNING, v1.Hook_CONDITION_SNAPSHOT_SUCCESS: + return v.renderTemplate(templateForSnapshotEnd) + default: + return v.renderTemplate(templateDefault) + } +} + +func (v HookVars) renderTemplate(templ string) (string, error) { + t, err := template.New("t").Parse(templ) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = t.Execute(&buf, v) + if err != nil { + return "", err + } + return buf.String(), nil +} + +var templateDefault = ` +{{ if .Error -}} +Backrest Error +Task: {{ .Task }} at {{ .FormatTime .CurTime }} +Event: {{ .EventName .Event }} +Repo: {{ .Repo.Id }} +Error: {{ .Error }} +{{ else -}} +Backrest Notification +Task: {{ .Task }} at {{ .FormatTime .CurTime }} +Event: {{ .EventName .Event }} +{{ end }} +` + +var templateForSnapshotEnd = ` +Backrest Snapshot Notification +Task: {{ .Task }} at {{ .FormatTime .CurTime }} +Event: {{ .EventName .Event }} +Snapshot: {{ .SnapshotId }} +{{ if .Error -}} +Error: {{ .Error }} +{{ else -}} +{{ if .SnapshotStats -}} + +Overview: +- Data added: {{ .FormatSizeBytes .SnapshotStats.DataAdded }} +- Total files processed: {{ .SnapshotStats.TotalFilesProcessed }} +- Total bytes processed: {{ .FormatSizeBytes .SnapshotStats.TotalBytesProcessed }} + +Backup Statistics: +- Files new: {{ .SnapshotStats.FilesNew }} +- Files changed: {{ .SnapshotStats.FilesChanged }} +- Files unmodified: {{ .SnapshotStats.FilesUnmodified }} +- Dirs new: {{ .SnapshotStats.DirsNew }} +- Dirs changed: {{ .SnapshotStats.DirsChanged }} +- Dirs unmodified: {{ .SnapshotStats.DirsUnmodified }} +- Data blobs: {{ .SnapshotStats.DataBlobs }} +- Tree blobs: {{ .SnapshotStats.TreeBlobs }} +- Total duration: {{ .SnapshotStats.TotalDuration }}s +{{ end }} +{{ end }}` + +var templateForSnapshotStart = ` +Backrest Notification for Snapshot Start +Task: "{{ .Task }}" at {{ .FormatTime .CurTime }} +Event: {{ .EventName .Event }} +Repo: {{ .Repo.Id }} +Plan: {{ .Plan.Id }} +Paths: +{{ range .Plan.Paths -}} + - {{ . }} +{{ end }}` diff --git a/internal/orchestrator/tasks/hookvars_test.go b/internal/orchestrator/tasks/hookvars_test.go new file mode 100644 index 000000000..a800d132b --- /dev/null +++ b/internal/orchestrator/tasks/hookvars_test.go @@ -0,0 +1,23 @@ +package tasks + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func TestHookVarsEventName(t *testing.T) { + // for every value of condition, check that the event name is correct + values := v1.Hook_Condition(0).Descriptor().Values() + for i := 0; i < values.Len(); i++ { + condition := v1.Hook_Condition(values.Get(i).Number()) + if condition == v1.Hook_CONDITION_UNKNOWN { + continue + } + + vars := HookVars{} + if vars.EventName(condition) == "unknown" { + t.Errorf("unexpected event name for condition %v", condition) + } + } +} diff --git a/internal/orchestrator/tasks/scheduling_test.go b/internal/orchestrator/tasks/scheduling_test.go new file mode 100644 index 000000000..9f41373a2 --- /dev/null +++ b/internal/orchestrator/tasks/scheduling_test.go @@ -0,0 +1,451 @@ +package tasks + +import ( + "os" + "runtime" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" +) + +func TestScheduling(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } + + os.Setenv("TZ", "America/Los_Angeles") + defer os.Unsetenv("TZ") + + cfg := &v1.Config{ + Instance: "instance1", + Repos: []*v1.Repo{ + { + Id: "repo1", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + }, + { + Id: "repo-absolute", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + }, + { + Id: "repo-relative", + Guid: cryptoutil.MustRandomID(cryptoutil.DefaultIDBits), + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, + }, + }, + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, + }, + }, + }, + }, + Plans: []*v1.Plan{ + { + Id: "plan-cron", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight + }, + Clock: v1.Schedule_CLOCK_LOCAL, + }, + }, + { + Id: "plan-cron-utc", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight + }, + Clock: v1.Schedule_CLOCK_UTC, + }, + }, + { + Id: "plan-cron-since-last-run", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight + }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, + }, + }, + { + Id: "plan-max-frequency-days", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + Clock: v1.Schedule_CLOCK_LOCAL, + }, + }, + { + Id: "plan-min-days-since-last-run", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, + }, + }, + { + Id: "plan-max-frequency-hours", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + { + Id: "plan-min-hours-since-last-run", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, + }, + }, + }, + } + + repo1 := config.FindRepo(cfg, "repo1") + repoAbsolute := config.FindRepo(cfg, "repo-absolute") + repoRelative := config.FindRepo(cfg, "repo-relative") + if repoAbsolute == nil || repoRelative == nil || repo1 == nil { + t.Fatalf("test config declaration error") + } + + now := time.Unix(100000, 0) // 1000 seconds after the epoch as an arbitrary time for the test + farFuture := time.Unix(999999, 0) + + tests := []struct { + name string + task Task + ops []*v1.Operation // operations in the log + wantTime time.Time // time to run the next task + }{ + { + name: "backup schedule max frequency days", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-max-frequency-days")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-max-frequency-days", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour * 24), + }, + { + name: "backup schedule min days since last run", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-min-days-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-min-days-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour * 24), + }, + { + name: "backup schedule max frequency hours", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-max-frequency-hours")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-max-frequency-hours", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "backup schedule min hours since last run", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-min-hours-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-min-hours-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + { + name: "backup schedule cron", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-cron")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-cron", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-02T00:00:00-08:00"), + }, + { + name: "backup schedule cron utc", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-cron-utc")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-cron-utc", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-02T08:00:00Z"), + }, + { + name: "backup schedule cron since last run", + task: NewScheduledBackupTask(config.FindRepo(cfg, "repo1"), config.FindPlan(cfg, "plan-cron-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + RepoGuid: repo1.Guid, + PlanId: "plan-cron-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-13T00:00:00-08:00"), + }, + { + name: "check schedule absolute", + task: NewCheckTask(repoAbsolute, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-absolute", + RepoGuid: repoAbsolute.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "check schedule relative no backup yet", + task: NewCheckTask(repoRelative, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "check schedule relative", + task: NewCheckTask(repoRelative, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + { + name: "prune schedule absolute", + task: NewPruneTask(repoAbsolute, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-absolute", + RepoGuid: repoAbsolute.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "prune schedule relative no backup yet", + task: NewPruneTask(repoRelative, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "prune schedule relative", + task: NewPruneTask(repoRelative, "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + { + InstanceId: "instance1", + RepoId: "repo-relative", + RepoGuid: repoRelative.Guid, + PlanId: "_system_", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + opstore, err := sqlitestore.NewMemorySqliteStore() + if err != nil { + t.Fatalf("failed to create opstore: %v", err) + } + for _, op := range tc.ops { + if err := opstore.Add(op); err != nil { + t.Fatalf("failed to add operation to opstore: %v", err) + } + } + + log, err := oplog.NewOpLog(opstore) + if err != nil { + t.Fatalf("failed to create oplog: %v", err) + } + + runner := newTestTaskRunner(t, cfg, log) + + st, err := tc.task.Next(now, runner) + if err != nil { + t.Fatalf("failed to get next task: %v", err) + } + + if !st.RunAt.Equal(tc.wantTime) { + t.Errorf("got run at %v, want %v", st.RunAt.Format(time.RFC3339), tc.wantTime.Format(time.RFC3339)) + } + }) + } +} + +func mustParseTime(t *testing.T, s string) time.Time { + t.Helper() + tm, err := time.Parse(time.RFC3339, s) + if err != nil { + t.Fatalf("failed to parse time: %v", err) + } + return tm +} diff --git a/internal/orchestrator/tasks/task.go b/internal/orchestrator/tasks/task.go new file mode 100644 index 000000000..eac2471ca --- /dev/null +++ b/internal/orchestrator/tasks/task.go @@ -0,0 +1,271 @@ +package tasks + +import ( + "context" + "errors" + "io" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +var NeverScheduledTask = ScheduledTask{} + +const ( + PlanForUnassociatedOperations = "_unassociated_" + InstanceIDForUnassociatedOperations = "_unassociated_" + PlanForSystemTasks = "_system_" // plan for system tasks e.g. garbage collection, prune, stats, etc. + + TaskPriorityStats = 0 + TaskPriorityDefault = 1 << 1 // default priority + TaskPriorityForget = 1 << 2 + TaskPriorityIndexSnapshots = 1 << 3 + TaskPriorityCheck = 1 << 4 // check should always run after prune. + TaskPriorityPrune = 1 << 5 + TaskPriorityInteractive = 1 << 6 // highest priority +) + +// TaskRunner is an interface for running tasks. It is used by tasks to create operations and write logs. +type TaskRunner interface { + // InstanceID returns the instance ID executing this task. + InstanceID() string + // GetOperation returns the operation with the given ID. + GetOperation(id int64) (*v1.Operation, error) + // CreateOperation creates the operation in storage and sets the operation ID in the task. + CreateOperation(...*v1.Operation) error + // UpdateOperation updates the operation in storage. It must be called after CreateOperation. + UpdateOperation(...*v1.Operation) error + // DeleteOperation deletes the operation from storage. + DeleteOperation(...int64) error + // QueryOperations queries the operation log. + QueryOperations(oplog.Query, func(*v1.Operation) error) error + // ExecuteHooks + ExecuteHooks(ctx context.Context, events []v1.Hook_Condition, vars HookVars) error + // GetRepo returns the repo with the given ID. + GetRepo(repoID string) (*v1.Repo, error) + // GetPlan returns the plan with the given ID. + GetPlan(planID string) (*v1.Plan, error) + // GetRepoOrchestrator returns the orchestrator for the repo with the given ID. + GetRepoOrchestrator(repoID string) (*repo.RepoOrchestrator, error) + // ScheduleTask schedules a task to run at a specific time. + ScheduleTask(task Task, priority int) error + // Config returns the current config. + Config() *v1.Config + // Logger returns the logger. + Logger(ctx context.Context) *zap.Logger + // LogrefWriter returns a writer that can be used to track streaming operation output. + LogrefWriter() (id string, w io.WriteCloser, err error) +} + +type TaskExecutor interface { + RunTask(ctx context.Context, st ScheduledTask) error +} + +// ScheduledTask is a task that is scheduled to run at a specific time. +type ScheduledTask struct { + Task Task // the task to run + RunAt time.Time // the time at which the task should be run. + Op *v1.Operation // operation associated with this execution of the task. +} + +func (s ScheduledTask) Eq(other ScheduledTask) bool { + return s.Task == other.Task && s.RunAt.Equal(other.RunAt) +} + +func (s ScheduledTask) Less(other ScheduledTask) bool { + if s.RunAt.Equal(other.RunAt) { + return s.Task.Name() < other.Task.Name() + } + return s.RunAt.Before(other.RunAt) +} + +// Task is a task that can be scheduled to run at a specific time. +type Task interface { + Name() string // human readable name for this task. + Type() string // simple string 'type' for this task. + Next(now time.Time, runner TaskRunner) (ScheduledTask, error) // returns the next scheduled task. + Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error // run the task. + PlanID() string // the ID of the plan this task is associated with. + RepoID() string // the ID of the repo this task is associated with. + Repo() *v1.Repo // the repo this task is associated with. +} + +type BaseTask struct { + TaskType string + TaskName string + TaskPlanID string + TaskRepo *v1.Repo +} + +func (b BaseTask) Type() string { + return b.TaskType +} + +func (b BaseTask) Name() string { + return b.TaskName +} + +func (b BaseTask) PlanID() string { + return b.TaskPlanID +} + +func (b BaseTask) RepoID() string { + if b.TaskRepo == nil { + return "" + } + return b.TaskRepo.Id +} + +func (b BaseTask) Repo() *v1.Repo { + return b.TaskRepo +} + +type OneoffTask struct { + BaseTask + RunAt time.Time + FlowID int64 // the ID of the flow this task is associated with. + DidSchedule bool + ProtoOp *v1.Operation // the prototype operation for this class of task. +} + +func (o *OneoffTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if o.DidSchedule { + return NeverScheduledTask, nil + } + o.DidSchedule = true + + var op *v1.Operation + if o.ProtoOp != nil { + if o.TaskRepo == nil { + return NeverScheduledTask, errors.New("task.repo must be provided if task.protoOp is provided") + } + op = proto.Clone(o.ProtoOp).(*v1.Operation) + op.RepoId = o.TaskRepo.Id + op.RepoGuid = o.TaskRepo.Guid + op.PlanId = o.TaskPlanID + op.FlowId = o.FlowID + op.UnixTimeStartMs = timeToUnixMillis(o.RunAt) // TODO: this should be updated before Run is called. + op.Status = v1.OperationStatus_STATUS_PENDING + } + + return ScheduledTask{ + RunAt: o.RunAt, + Op: op, + }, nil +} + +type GenericOneoffTask struct { + OneoffTask + Do func(ctx context.Context, st ScheduledTask, runner TaskRunner) error +} + +func (g *GenericOneoffTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + return g.Do(ctx, st, runner) +} + +func timeToUnixMillis(t time.Time) int64 { + return t.Unix()*1000 + int64(t.Nanosecond()/1000000) +} + +func curTimeMillis() int64 { + return timeToUnixMillis(time.Now()) +} + +type testTaskRunner struct { + config *v1.Config // the config to use for the task runner. + oplog *oplog.OpLog +} + +var _ TaskRunner = &testTaskRunner{} + +func newTestTaskRunner(_ testing.TB, config *v1.Config, oplog *oplog.OpLog) *testTaskRunner { + return &testTaskRunner{ + config: config, + oplog: oplog, + } +} + +func (t *testTaskRunner) InstanceID() string { + return t.config.Instance +} + +func (t *testTaskRunner) GetOperation(id int64) (*v1.Operation, error) { + return t.oplog.Get(id) +} + +func (t *testTaskRunner) CreateOperation(op ...*v1.Operation) error { + for _, o := range op { + if o.InstanceId != "" { + continue + } + o.InstanceId = t.InstanceID() + } + return t.oplog.Add(op...) +} + +func (t *testTaskRunner) UpdateOperation(op ...*v1.Operation) error { + for _, o := range op { + if o.InstanceId != "" { + continue + } + o.InstanceId = t.InstanceID() + } + return t.oplog.Update(op...) +} + +func (t *testTaskRunner) DeleteOperation(id ...int64) error { + return t.oplog.Delete(id...) +} + +func (t *testTaskRunner) ExecuteHooks(ctx context.Context, events []v1.Hook_Condition, vars HookVars) error { + panic("not implemented") +} + +func (t *testTaskRunner) QueryOperations(q oplog.Query, fn func(*v1.Operation) error) error { + if q.InstanceID == nil { + q.SetInstanceID(t.InstanceID()) + } + return t.oplog.Query(q, fn) +} + +func (t *testTaskRunner) GetRepo(repoID string) (*v1.Repo, error) { + cfg := config.FindRepo(t.config, repoID) + if cfg == nil { + return nil, errors.New("repo not found") + } + return cfg, nil +} + +func (t *testTaskRunner) GetPlan(planID string) (*v1.Plan, error) { + cfg := config.FindPlan(t.config, planID) + if cfg == nil { + return nil, errors.New("plan not found") + } + return cfg, nil +} + +func (t *testTaskRunner) GetRepoOrchestrator(repoID string) (*repo.RepoOrchestrator, error) { + panic("not implemented") +} + +func (t *testTaskRunner) ScheduleTask(task Task, priority int) error { + panic("not implemented") +} + +func (t *testTaskRunner) Config() *v1.Config { + return t.config +} + +func (t *testTaskRunner) Logger(ctx context.Context) *zap.Logger { + return zap.L() +} + +func (t *testTaskRunner) LogrefWriter() (id string, w io.WriteCloser, err error) { + panic("not implemented") +} diff --git a/internal/orchestrator/tasks/taskbackup.go b/internal/orchestrator/tasks/taskbackup.go new file mode 100644 index 000000000..9d95f0d6e --- /dev/null +++ b/internal/orchestrator/tasks/taskbackup.go @@ -0,0 +1,258 @@ +package tasks + +import ( + "context" + "errors" + "fmt" + "slices" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/metric" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "github.com/garethgeorge/backrest/pkg/restic" + "go.uber.org/zap" +) + +var maxBackupErrorHistoryLength = 20 // arbitrary limit on the number of file read errors recorded in a backup operation to prevent it from growing too large. + +// BackupTask is a scheduled backup operation. +type BackupTask struct { + BaseTask + force bool + didRun bool +} + +var _ Task = &BackupTask{} + +func NewScheduledBackupTask(repo *v1.Repo, plan *v1.Plan) *BackupTask { + return &BackupTask{ + BaseTask: BaseTask{ + TaskType: "backup", + TaskName: fmt.Sprintf("backup for plan %q", plan.Id), + TaskRepo: repo, + TaskPlanID: plan.Id, + }, + } +} + +func NewOneoffBackupTask(repo *v1.Repo, plan *v1.Plan, at time.Time) *BackupTask { + return &BackupTask{ + BaseTask: BaseTask{ + TaskType: "backup", + TaskName: fmt.Sprintf("backup for plan %q", plan.Id), + TaskRepo: repo, + TaskPlanID: plan.Id, + }, + force: true, + } +} + +func (t *BackupTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if t.force { + if t.didRun { + return NeverScheduledTask, nil + } + t.didRun = true + return ScheduledTask{ + Task: t, + RunAt: now, + Op: &v1.Operation{ + Op: &v1.Operation_OperationBackup{}, + }, + }, nil + } + + plan, err := runner.GetPlan(t.PlanID()) + if err != nil { + return NeverScheduledTask, err + } + + if plan.Schedule == nil { + return NeverScheduledTask, nil + } + + var lastRan time.Time + if err := runner.QueryOperations(oplog.Query{}. + SetInstanceID(runner.InstanceID()). + SetRepoGUID(t.Repo().GetGuid()). + SetPlanID(t.PlanID()). + SetReversed(true), func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationBackup); ok && op.UnixTimeEndMs != 0 { + lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) + return oplog.ErrStopIteration + } + return nil + }); err != nil { + return NeverScheduledTask, fmt.Errorf("finding last backup run time: %w", err) + } else if lastRan.IsZero() { + lastRan = time.Now() + } + + nextRun, err := protoutil.ResolveSchedule(plan.Schedule, lastRan, now) + if errors.Is(err, protoutil.ErrScheduleDisabled) { + return NeverScheduledTask, nil + } else if err != nil { + return NeverScheduledTask, fmt.Errorf("resolving schedule: %w", err) + } + + return ScheduledTask{ + Task: t, + RunAt: nextRun, + Op: &v1.Operation{ + Op: &v1.Operation_OperationBackup{}, + }, + }, nil +} + +func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + l := runner.Logger(ctx) + + startTime := time.Now() + op := st.Op + backupOp := &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + } + op.Op = backupOp + + repo, err := runner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return err + } + + if err := repo.UnlockIfAutoEnabled(ctx); err != nil { + return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err) + } + + plan, err := runner.GetPlan(t.PlanID()) + if err != nil { + return err + } + + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, HookVars{}); err != nil { + return fmt.Errorf("snapshot start hook: %w", err) + } + + var sendWg sync.WaitGroup + lastSent := time.Now() // debounce progress updates, these can endup being very frequent. + var lastFiles []string + fileErrorCount := 0 + summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { + sendWg.Wait() + if entry.MessageType == "status" { + // prevents flickering output when a status entry omits the CurrentFiles property. Largely cosmetic. + if len(entry.CurrentFiles) == 0 { + entry.CurrentFiles = lastFiles + } else { + lastFiles = entry.CurrentFiles + } + + backupOp.OperationBackup.LastStatus = protoutil.BackupProgressEntryToProto(entry) + } else if entry.MessageType == "error" { + l.Sugar().Warnf("an unknown error was encountered in processing item: %v", entry.Item) + fileErrorCount++ + backupError, err := protoutil.BackupProgressEntryToBackupError(entry) + if err != nil { + l.Sugar().Errorf("failed to convert backup progress entry to backup error: %v", err) + return + } + if len(backupOp.OperationBackup.Errors) > maxBackupErrorHistoryLength || + slices.ContainsFunc(backupOp.OperationBackup.Errors, func(i *v1.BackupProgressError) bool { + return i.Item == backupError.Item + }) { + return + } + backupOp.OperationBackup.Errors = append(backupOp.OperationBackup.Errors, backupError) + } else if entry.MessageType != "summary" { + zap.S().Warnf("unexpected message type %q in backup progress entry", entry.MessageType) + } + + if time.Since(lastSent) <= 1000*time.Millisecond { + return + } + lastSent = time.Now() + + sendWg.Add(1) + go func() { + if err := runner.UpdateOperation(op); err != nil { + l.Sugar().Errorf("failed to update oplog with progress for backup: %v", err) + } + sendWg.Done() + }() + }) + sendWg.Wait() + + if summary == nil { + summary = &restic.BackupProgressEntry{} + } + + metric.GetRegistry().RecordBackupSummary(t.RepoID(), t.PlanID(), summary.TotalBytesProcessed, summary.DataAdded, int64(fileErrorCount)) + + vars := HookVars{ + Task: t.Name(), + SnapshotStats: summary, + SnapshotId: summary.SnapshotId, + } + + if err != nil { + vars.Error = err.Error() + if !errors.Is(err, restic.ErrPartialBackup) { + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_ERROR, + v1.Hook_CONDITION_ANY_ERROR, + v1.Hook_CONDITION_SNAPSHOT_END, + }, vars) + return err + } else { + vars.Error = fmt.Sprintf("partial backup, %d files may not have been read completely.", len(backupOp.OperationBackup.Errors)) + } + op.Status = v1.OperationStatus_STATUS_WARNING + op.DisplayMessage = "Partial backup, some files may not have been read completely." + + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_WARNING, + v1.Hook_CONDITION_SNAPSHOT_END, + }, vars) + } + + op.SnapshotId = summary.SnapshotId + backupOp.OperationBackup.LastStatus = protoutil.BackupProgressEntryToProto(summary) + if backupOp.OperationBackup.LastStatus == nil { + return fmt.Errorf("expected a final backup progress entry, got nil") + } + + l.Info("backup complete", zap.String("plan", plan.Id), zap.Duration("duration", time.Since(startTime)), zap.Any("summary", backupOp.OperationBackup.LastStatus)) + + if summary.SnapshotId == "" { // support --skip-if-unchanged which returns an operation with an empty snapshot ID + op.DisplayMessage = "No snapshot added, possibly due to no changes in the source data." + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_SKIPPED, + v1.Hook_CONDITION_SNAPSHOT_END, + }, vars) + } else { + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_SUCCESS, + v1.Hook_CONDITION_SNAPSHOT_END, + }, vars) + + // schedule followup tasks if a snapshot was added + at := time.Now() + if _, ok := plan.Retention.GetPolicy().(*v1.RetentionPolicy_PolicyKeepAll); plan.Retention != nil && !ok { + if err := runner.ScheduleTask(NewOneoffForgetTask(t.Repo(), t.PlanID(), op.FlowId, at), TaskPriorityForget); err != nil { + return fmt.Errorf("failed to schedule forget task: %w", err) + } + } + if err := runner.ScheduleTask(NewOneoffIndexSnapshotsTask(t.Repo(), at), TaskPriorityIndexSnapshots); err != nil { + return fmt.Errorf("failed to schedule index snapshots task: %w", err) + } + } + + return nil +} diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go new file mode 100644 index 000000000..ca0464e30 --- /dev/null +++ b/internal/orchestrator/tasks/taskcheck.go @@ -0,0 +1,154 @@ +package tasks + +import ( + "context" + "errors" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" +) + +type CheckTask struct { + BaseTask + force bool + didRun bool +} + +func NewCheckTask(repo *v1.Repo, planID string, force bool) Task { + return &CheckTask{ + BaseTask: BaseTask{ + TaskType: "check", + TaskName: fmt.Sprintf("check for repo %q", repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + force: force, + } +} + +func (t *CheckTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if t.force { + if t.didRun { + return NeverScheduledTask, nil + } + t.didRun = true + return ScheduledTask{ + Task: t, + RunAt: now, + Op: &v1.Operation{ + Op: &v1.Operation_OperationCheck{}, + }, + }, nil + } + + repo, err := runner.GetRepo(t.RepoID()) + if err != nil { + return ScheduledTask{}, fmt.Errorf("get repo %v: %w", t.RepoID(), err) + } + + if repo.CheckPolicy.GetSchedule() == nil { + return NeverScheduledTask, nil + } + + var lastRan time.Time + var foundBackup bool + + if err := runner.QueryOperations(oplog.Query{}. + SetInstanceID(runner.InstanceID()). // note: this means that check tasks run by remote instances are ignored. + SetRepoGUID(t.Repo().GetGuid()). + SetReversed(true), func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationCheck); ok && op.UnixTimeEndMs != 0 { + lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) + return oplog.ErrStopIteration + } + if _, ok := op.Op.(*v1.Operation_OperationBackup); ok { + foundBackup = true + } + return nil + }); err != nil { + return NeverScheduledTask, fmt.Errorf("finding last check run time: %w", err) + } else if !foundBackup { + lastRan = now + } + + runAt, err := protoutil.ResolveSchedule(repo.CheckPolicy.GetSchedule(), lastRan, now) + if errors.Is(err, protoutil.ErrScheduleDisabled) { + return NeverScheduledTask, nil + } else if err != nil { + return NeverScheduledTask, fmt.Errorf("resolve schedule: %w", err) + } + + return ScheduledTask{ + Task: t, + RunAt: runAt, + Op: &v1.Operation{ + Op: &v1.Operation_OperationCheck{}, + }, + }, nil +} + +func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + op := st.Op + + repo, err := runner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("couldn't get repo %q: %w", t.RepoID(), err) + } + + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_CHECK_START, + }, HookVars{}); err != nil { + return fmt.Errorf("check start hook: %w", err) + } + + err = repo.UnlockIfAutoEnabled(ctx) + if err != nil { + return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err) + } + + opCheck := &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + } + op.Op = opCheck + + liveID, writer, err := runner.LogrefWriter() + if err != nil { + return fmt.Errorf("create logref writer: %w", err) + } + defer writer.Close() + opCheck.OperationCheck.OutputLogref = liveID + + if err := runner.UpdateOperation(op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + err = repo.Check(ctx, writer) + if err != nil { + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_CHECK_ERROR, + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Error: err.Error(), + }) + + return fmt.Errorf("check: %w", err) + } + + if err := writer.Close(); err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_CHECK_SUCCESS, + }, HookVars{}); err != nil { + return fmt.Errorf("execute check success hooks: %w", err) + } + + return nil +} diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go new file mode 100644 index 000000000..866b6667b --- /dev/null +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -0,0 +1,209 @@ +package tasks + +import ( + "context" + "fmt" + "reflect" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/logstore" + "github.com/garethgeorge/backrest/internal/oplog" + "go.uber.org/zap" +) + +type gcSettingsForType struct { + maxAge time.Duration + keepMin int + keepMax int +} + +type groupByKey struct { + RepoID string + RepoGUID string + PlanID string + InstanceID string + Type reflect.Type +} + +const ( + gcStartupDelay = 1 * time.Second + gcInterval = 24 * time.Hour +) + +var gcSettings = map[reflect.Type]gcSettingsForType{ + reflect.TypeOf(&v1.Operation_OperationStats{}): { + maxAge: 365 * 24 * time.Hour, + keepMin: 1, + keepMax: 100, + }, + reflect.TypeOf(&v1.Operation_OperationCheck{}): { + maxAge: 365 * 24 * time.Hour, + keepMin: 1, + keepMax: 12, + }, + reflect.TypeOf(&v1.Operation_OperationPrune{}): { + maxAge: 365 * 24 * time.Hour, + keepMin: 1, + keepMax: 12, + }, +} + +var defaultGcSettings = gcSettingsForType{ + maxAge: 30 * 24 * time.Hour, + keepMin: 1, + keepMax: 100, +} + +type CollectGarbageTask struct { + BaseTask + firstRun bool + logstore *logstore.LogStore +} + +func NewCollectGarbageTask(logstore *logstore.LogStore) *CollectGarbageTask { + return &CollectGarbageTask{ + BaseTask: BaseTask{ + TaskType: "collect_garbage", + TaskName: "collect garbage", + }, + logstore: logstore, + } +} + +var _ Task = &CollectGarbageTask{} + +func (t *CollectGarbageTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if !t.firstRun { + t.firstRun = true + runAt := now.Add(gcStartupDelay) + return ScheduledTask{ + Task: t, + RunAt: runAt, + }, nil + } + + runAt := now.Add(gcInterval) + return ScheduledTask{ + Task: t, + RunAt: runAt, + }, nil +} + +func (t *CollectGarbageTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + if err := t.gcOperations(runner); err != nil { + return fmt.Errorf("collecting garbage: %w", err) + } + + return nil +} + +func (t *CollectGarbageTask) gcOperations(runner TaskRunner) error { + // snapshotForgottenForFlow returns whether the snapshot associated with the flow is forgotten + snapshotForgottenForFlow := make(map[int64]bool) + if err := runner.QueryOperations(oplog.SelectAll, func(op *v1.Operation) error { + if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { + snapshotForgottenForFlow[op.FlowId] = snapshotOp.OperationIndexSnapshot.Forgot + } + return nil + }); err != nil { + return fmt.Errorf("identifying forgotten snapshots: %w", err) + } + + // keep track of IDs that are still valid and of the IDs that are being forgotten + validIDs := make(map[int64]struct{}) + forgetIDs := []int64{} + curTime := curTimeMillis() + + var deletedByMaxAge, deletedByMaxCount, deletedByForgottenSnapshot int + deletedByType := make(map[string]int) + stats := make(map[groupByKey]gcSettingsForType) + + if err := runner.QueryOperations(oplog.Query{}.SetReversed(true), func(op *v1.Operation) error { + validIDs[op.Id] = struct{}{} + + forgot, ok := snapshotForgottenForFlow[op.FlowId] + if ok { + if forgot { + // snapshot is forgotten; this operation is eligible for gc + forgetIDs = append(forgetIDs, op.Id) + deletedByForgottenSnapshot++ + deletedByType[reflect.TypeOf(op.Op).String()]++ + } + return nil + } + + key := groupByKey{ + RepoGUID: op.RepoGuid, + RepoID: op.RepoId, + PlanID: op.PlanId, + InstanceID: op.InstanceId, + Type: reflect.TypeOf(op.Op), + } + + st, ok := stats[key] + if !ok { + gcSettings, ok := gcSettings[reflect.TypeOf(op.Op)] + if !ok { + st = defaultGcSettings + } else { + st = gcSettings + } + } + + st.keepMax-- // decrement the max retention, when this < 0 operation must be gc'd + st.keepMin-- // decrement the min retention, when this < 0 we can start gc'ing + stats[key] = st // update the stats + + if st.keepMin >= 0 { + // can't delete if within min retention period + return nil + } + if st.keepMax < 0 { + // max retention reached; this operation must be gc'd. + forgetIDs = append(forgetIDs, op.Id) + deletedByMaxCount++ + deletedByType[key.Type.String()]++ + } else if curTime-op.UnixTimeStartMs > st.maxAge.Milliseconds() { + // operation is old enough to be gc'd + forgetIDs = append(forgetIDs, op.Id) + deletedByMaxAge++ + deletedByType[key.Type.String()]++ + } + + return nil + }); err != nil { + return fmt.Errorf("identifying gc eligible operations: %w", err) + } + + if err := runner.DeleteOperation(forgetIDs...); err != nil { + return fmt.Errorf("removing gc eligible operations: %w", err) + } + for _, id := range forgetIDs { // update validIDs with respect to the just deleted operations + delete(validIDs, id) + } + + zap.L().Info("collecting garbage operations", + zap.Int("operations_removed", len(forgetIDs)), zap.Int("removed_by_age", deletedByMaxAge), zap.Int("removed_by_limit", deletedByMaxCount), zap.Int("removed_by_snapshot_forgotten", deletedByForgottenSnapshot), zap.Any("removed_by_type", deletedByType)) + + // cleaning up logstore + toDelete := []string{} + if err := t.logstore.SelectAll(func(id string, parentID int64) { + if parentID == 0 { + return + } + if _, ok := validIDs[parentID]; !ok { + toDelete = append(toDelete, id) + } + }); err != nil { + return fmt.Errorf("selecting all logstore entries: %w", err) + } + for _, id := range toDelete { + if err := t.logstore.Delete(id); err != nil { + zap.L().Error("deleting logstore entry", zap.String("id", id), zap.Error(err)) + } + } + zap.L().Info("collecting garbage logs", zap.Any("logs_removed", len(toDelete))) + + return nil +} diff --git a/internal/orchestrator/tasks/taskforget.go b/internal/orchestrator/tasks/taskforget.go new file mode 100644 index 000000000..86df79005 --- /dev/null +++ b/internal/orchestrator/tasks/taskforget.go @@ -0,0 +1,156 @@ +package tasks + +import ( + "context" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "github.com/hashicorp/go-multierror" + "go.uber.org/zap" +) + +func NewOneoffForgetTask(repo *v1.Repo, planID string, flowID int64, at time.Time) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "forget", + TaskName: fmt.Sprintf("forget for plan %q in repo %q", repo.Id, planID), + TaskRepo: repo, + TaskPlanID: planID, + }, + FlowID: flowID, + RunAt: at, + ProtoOp: &v1.Operation{ + Op: &v1.Operation_OperationForget{}, + }, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + op := st.Op + forgetOp := op.GetOperationForget() + if forgetOp == nil { + panic("forget task with non-forget operation") + } + + return forgetHelper(ctx, st, taskRunner) + }, + } +} + +func forgetHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + t := st.Task + l := taskRunner.Logger(ctx) + + r, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("get repo %q: %w", t.RepoID(), err) + } + + err = r.UnlockIfAutoEnabled(ctx) + if err != nil { + return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err) + } + + plan, err := taskRunner.GetPlan(t.PlanID()) + if err != nil { + return fmt.Errorf("get plan %q: %w", t.PlanID(), err) + } + + // execute hooks + if err := taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_FORGET_START, + }, HookVars{Plan: plan}); err != nil { + return fmt.Errorf("forget start hook: %w", err) + } + + tags := []string{repo.TagForPlan(t.PlanID())} + if compat, err := useLegacyCompatMode(l, taskRunner, t.Repo().GetGuid(), t.PlanID()); err != nil { + return fmt.Errorf("check legacy compat mode: %w", err) + } else if !compat { + tags = append(tags, repo.TagForInstance(taskRunner.Config().Instance)) + } else { + l.Warn("forgetting snapshots without instance ID, using legacy behavior (e.g. --tags not including instance ID)") + l.Sugar().Warnf("to avoid this warning, tag all snapshots with the instance ID e.g. by running: \r\n"+ + "restic tag --set '%s' --set '%s' --tag '%s'", repo.TagForPlan(t.PlanID()), repo.TagForInstance(taskRunner.Config().Instance), repo.TagForPlan(t.PlanID())) + } + + // check if any other instance IDs exist in the repo (unassociated don't count) + forgot, err := r.Forget(ctx, plan, tags) + + forgetOp := &v1.Operation_OperationForget{ + OperationForget: &v1.OperationForget{}, + } + st.Op.Op = forgetOp + + forgetOp.OperationForget.Forget = append(forgetOp.OperationForget.Forget, forgot...) + forgetOp.OperationForget.Policy = plan.Retention + + var ops []*v1.Operation + for _, forgot := range forgot { + if e := taskRunner.QueryOperations(oplog.Query{}. + SetRepoGUID(t.Repo().GetGuid()). + SetSnapshotID(forgot.Id), func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); e != nil { + err = multierror.Append(err, fmt.Errorf("cleanup snapshot %v: %w", forgot.Id, e)) + } + } + + l.Sugar().Debugf("found %v snapshots were forgotten, marking this in oplog", len(ops)) + + for _, op := range ops { + if indexOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { + indexOp.OperationIndexSnapshot.Forgot = true + if e := taskRunner.UpdateOperation(op); err != nil { + err = multierror.Append(err, fmt.Errorf("mark index snapshot %v as forgotten: %w", op.Id, e)) + continue + } + } + } + + if err != nil { + if e := taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_FORGET_ERROR, + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Error: err.Error(), + }); e != nil { + err = multierror.Append(err, fmt.Errorf("forget on error hook: %w", e)) + } + return fmt.Errorf("forget: %w", err) + } else if e := taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_FORGET_SUCCESS, + }, HookVars{}); e != nil { + return fmt.Errorf("forget end hook: %w", e) + } + + return err +} + +// useLegacyCompatMode checks if there are any snapshots that were created without a `created-by` tag still exist in the repo. +// The property is overridden if mixed `created-by` tag values are found. +func useLegacyCompatMode(l *zap.Logger, taskRunner TaskRunner, repoGUID, planID string) (bool, error) { + instanceIDs := make(map[string]struct{}) + if err := taskRunner.QueryOperations(oplog.Query{}.SetRepoGUID(repoGUID).SetPlanID(planID).SetReversed(true), func(op *v1.Operation) error { + if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok && !snapshotOp.OperationIndexSnapshot.GetForgot() { + tags := snapshotOp.OperationIndexSnapshot.GetSnapshot().GetTags() + instanceIDs[repo.InstanceIDFromTags(tags)] = struct{}{} + } + return nil + }); err != nil { + return false, err + } + if _, ok := instanceIDs[""]; !ok { + return false, nil + } + delete(instanceIDs, "") + if len(instanceIDs) > 1 { + l.Sugar().Warn("found mixed instance IDs in indexed snapshots, overriding legacy forget behavior to include instance ID tags. This may result in unexpected behavior -- please inspect the tags on your snapshots.") + return false, nil + } + l.Sugar().Warn("found legacy snapshots without instance ID, recommending legacy forget behavior.") + return true, nil +} diff --git a/internal/orchestrator/tasks/taskforgetsnapshot.go b/internal/orchestrator/tasks/taskforgetsnapshot.go new file mode 100644 index 000000000..f60328bb9 --- /dev/null +++ b/internal/orchestrator/tasks/taskforgetsnapshot.go @@ -0,0 +1,67 @@ +package tasks + +import ( + "context" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func NewOneoffForgetSnapshotTask(repo *v1.Repo, planID string, flowID int64, at time.Time, snapshotID string) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "forget_snapshot", + TaskName: fmt.Sprintf("forget snapshot %q for plan %q in repo %q", snapshotID, planID, repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + FlowID: flowID, + RunAt: at, + ProtoOp: &v1.Operation{ + Op: &v1.Operation_OperationForget{}, + }, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + op := st.Op + forgetOp := op.GetOperationForget() + if forgetOp == nil { + panic("forget task with non-forget operation") + } + + if err := forgetSnapshotHelper(ctx, st, taskRunner, snapshotID); err != nil { + taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Error: err.Error(), + }) + return err + } + return nil + }, + } +} + +func forgetSnapshotHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, snapshotID string) error { + t := st.Task + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("get repo %q: %w", t.RepoID(), err) + } + + err = repo.UnlockIfAutoEnabled(ctx) + if err != nil { + return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err) + } + + if err := repo.ForgetSnapshot(ctx, snapshotID); err != nil { + return fmt.Errorf("forget %q: %w", snapshotID, err) + } + + taskRunner.ScheduleTask(NewOneoffIndexSnapshotsTask(t.Repo(), time.Now()), TaskPriorityIndexSnapshots) + taskRunner.DeleteOperation(st.Op.Id) + st.Op = nil + return err +} diff --git a/internal/orchestrator/tasks/taskindexsnapshots.go b/internal/orchestrator/tasks/taskindexsnapshots.go new file mode 100644 index 000000000..0380c9a81 --- /dev/null +++ b/internal/orchestrator/tasks/taskindexsnapshots.go @@ -0,0 +1,216 @@ +package tasks + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/orchestrator/repo" + "github.com/garethgeorge/backrest/internal/protoutil" + "github.com/garethgeorge/backrest/pkg/restic" + "go.uber.org/zap" +) + +func NewOneoffIndexSnapshotsTask(repo *v1.Repo, at time.Time) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "index_snapshots", + TaskName: fmt.Sprintf("index snapshots for repo %q", repo.Id), + TaskRepo: repo, + }, + RunAt: at, + ProtoOp: nil, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + if err := indexSnapshotsHelper(ctx, st, taskRunner); err != nil { + taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Task: st.Task.Name(), + Error: err.Error(), + }) + return err + } + return nil + }, + } +} + +// indexSnapshotsHelper indexes all snapshots for a plan. +// - If the snapshot is already indexed, it is skipped. +// - If the snapshot is not indexed, an index snapshot operation with it's metadata is added. +// - If an index snapshot operation is found for a snapshot that is not returned by the repo, it is marked as forgotten. +func indexSnapshotsHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + t := st.Task + l := taskRunner.Logger(ctx) + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("couldn't get repo %q: %w", t.RepoID(), err) + } + + // collect all tracked snapshots for the plan. + snapshots, err := repo.Snapshots(ctx) + if err != nil { + return fmt.Errorf("get snapshots for repo %q: %w", t.RepoID(), err) + } + + // collect all current snapshot IDs. + currentIds, err := indexCurrentSnapshotIdsForRepo(taskRunner, t.Repo().GetGuid()) + if err != nil { + return fmt.Errorf("get known snapshot IDs for repo %q: %w", t.RepoID(), err) + } + + foundIds := make(map[string]struct{}) + + // Index newly found operations + var indexOps []*v1.Operation + for _, snapshot := range snapshots { + if _, ok := currentIds[snapshot.Id]; ok { + foundIds[snapshot.Id] = struct{}{} + continue + } + + snapshotProto := protoutil.SnapshotToProto(snapshot) + flowID, err := FlowIDForSnapshotID(taskRunner, t.Repo().GetGuid(), snapshot.Id) + if err != nil { + return fmt.Errorf("get flow ID for snapshot %q: %w", snapshot.Id, err) + } + planId := planForSnapshot(snapshotProto) + instanceID := instanceIDForSnapshot(snapshotProto) + indexOps = append(indexOps, &v1.Operation{ + RepoId: t.Repo().Id, + RepoGuid: t.Repo().Guid, + PlanId: planId, + FlowId: flowID, + InstanceId: instanceID, + UnixTimeStartMs: snapshotProto.UnixTimeMs, + UnixTimeEndMs: snapshotProto.UnixTimeMs + snapshot.SnapshotSummary.DurationMs(), + Status: v1.OperationStatus_STATUS_SUCCESS, + SnapshotId: snapshotProto.Id, + Op: &v1.Operation_OperationIndexSnapshot{ + OperationIndexSnapshot: &v1.OperationIndexSnapshot{ + Snapshot: snapshotProto, + }, + }, + }) + } + + l.Sugar().Debugf("adding %v new snapshots to the oplog", len(indexOps)) + l.Sugar().Debugf("found %v snapshots already indexed", len(foundIds)) + + if err := taskRunner.CreateOperation(indexOps...); err != nil { + return fmt.Errorf("Create snapshot operations: %w", err) + } + + // Mark missing operations as newly forgotten. + for id, opId := range currentIds { + if _, ok := foundIds[id]; ok { + // skip snapshots that were found. + continue + } + + // mark snapshot forgotten. + op, err := taskRunner.GetOperation(opId) + if err != nil { + // should only be possible in the case of a data race (e.g. operation was somehow deleted). + return fmt.Errorf("get operation %v: %w", opId, err) + } + + snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot) + if !ok { + return fmt.Errorf("operation %v is not an index snapshot operation", opId) + } + snapshotOp.OperationIndexSnapshot.Forgot = true + + if err := taskRunner.UpdateOperation(op); err != nil { + return fmt.Errorf("mark index snapshot operation %v as forgotten: %w", opId, err) + } + } + + l.Sugar().Debugf("marked %v snapshots as forgotten", len(currentIds)-len(foundIds)) + + return err +} + +// returns a map of current (e.g. not forgotten) snapshot IDs for the plan. +func indexCurrentSnapshotIdsForRepo(taskRunner TaskRunner, repoGUID string) (map[string]int64, error) { + knownIds := make(map[string]int64) + + startTime := time.Now() + if err := taskRunner.QueryOperations(oplog.Query{}. + SetRepoGUID(repoGUID), + func(op *v1.Operation) error { + if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { + if !snapshotOp.OperationIndexSnapshot.Forgot { + knownIds[snapshotOp.OperationIndexSnapshot.Snapshot.Id] = op.Id + } + } + return nil + }); err != nil { + return nil, err + } + zap.S().Debugf("found %v known snapshot IDs for repo %v in %v", len(knownIds), repoGUID, time.Since(startTime)) + return knownIds, nil +} + +func planForSnapshot(snapshot *v1.ResticSnapshot) string { + p := repo.PlanFromTags(snapshot.Tags) + if p != "" { + return p + } + return PlanForUnassociatedOperations +} + +func instanceIDForSnapshot(snapshot *v1.ResticSnapshot) string { + id := repo.InstanceIDFromTags(snapshot.Tags) + if id != "" { + return id + } + return InstanceIDForUnassociatedOperations +} + +// tryMigrate checks if the snapshots use the latest backrest tag set and migrates them if necessary. +func tryMigrate(ctx context.Context, repo *repo.RepoOrchestrator, config *v1.Config, snapshots []*restic.Snapshot) (bool, error) { + if config.Instance == "" { + zap.S().Warnf("Instance ID not set. Skipping migration.") + return false, nil + } + + planIDs := make(map[string]struct{}) + for _, plan := range config.Plans { + planIDs[plan.Id] = struct{}{} + } + + needsCreatedBy := []string{} + for _, snapshot := range snapshots { + // Check if snapshot is already tagged with `created-by:`` + if idx := slices.IndexFunc(snapshot.Tags, func(tag string) bool { + return strings.HasPrefix(tag, "created-by:") + }); idx != -1 { + continue + } + // Check that snapshot is included in a plan for this instance. Backrest will not take ownership of snapshots belonging to it isn't aware of. + if _, ok := planIDs[planForSnapshot(protoutil.SnapshotToProto(snapshot))]; !ok { + continue + } + needsCreatedBy = append(needsCreatedBy, snapshot.Id) + } + + if len(needsCreatedBy) == 0 { + return false, nil + } + + zap.S().Warnf("Found %v snapshots without created-by tag but included in a plan for this instance. Taking ownership and adding created-by tag.", len(needsCreatedBy)) + + if err := repo.AddTags(ctx, needsCreatedBy, []string{fmt.Sprintf("created-by:%v", config.Instance)}); err != nil { + return false, fmt.Errorf("add created-by tag to snapshots: %w", err) + } + + return true, nil +} diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go new file mode 100644 index 000000000..345d23173 --- /dev/null +++ b/internal/orchestrator/tasks/taskprune.go @@ -0,0 +1,159 @@ +package tasks + +import ( + "context" + "errors" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.uber.org/zap" +) + +type PruneTask struct { + BaseTask + force bool + didRun bool +} + +func NewPruneTask(repo *v1.Repo, planID string, force bool) Task { + return &PruneTask{ + BaseTask: BaseTask{ + TaskType: "prune", + TaskName: fmt.Sprintf("prune repo %q", repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + force: force, + } +} + +func (t *PruneTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if t.force { + if t.didRun { + return NeverScheduledTask, nil + } + t.didRun = true + return ScheduledTask{ + Task: t, + RunAt: now, + Op: &v1.Operation{ + Op: &v1.Operation_OperationPrune{}, + }, + }, nil + } + + repo, err := runner.GetRepo(t.RepoID()) + if err != nil { + return ScheduledTask{}, fmt.Errorf("get repo %v: %w", t.RepoID(), err) + } + + if repo.PrunePolicy.GetSchedule() == nil { + return NeverScheduledTask, nil + } + + var lastRan time.Time + var foundBackup bool + if err := runner.QueryOperations(oplog.Query{}. + SetInstanceID(runner.InstanceID()). // note: this means that prune tasks run by remote instances are ignored. + SetRepoGUID(repo.GetGuid()). + SetReversed(true), func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationPrune); ok && op.UnixTimeEndMs != 0 { + lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) + return oplog.ErrStopIteration + } + if _, ok := op.Op.(*v1.Operation_OperationBackup); ok { + foundBackup = true + } + return nil + }); err != nil { + return NeverScheduledTask, fmt.Errorf("finding last prune run time: %w", err) + } else if !foundBackup { + lastRan = now + } + + runAt, err := protoutil.ResolveSchedule(repo.PrunePolicy.GetSchedule(), lastRan, now) + if errors.Is(err, protoutil.ErrScheduleDisabled) { + return NeverScheduledTask, nil + } else if err != nil { + return NeverScheduledTask, fmt.Errorf("resolve schedule: %w", err) + } + + return ScheduledTask{ + Task: t, + RunAt: runAt, + Op: &v1.Operation{ + Op: &v1.Operation_OperationPrune{}, + }, + }, nil +} + +func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + op := st.Op + + repo, err := runner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("couldn't get repo %q: %w", t.RepoID(), err) + } + + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_PRUNE_START, + }, HookVars{}); err != nil { + return fmt.Errorf("prune start hook: %w", err) + } + + err = repo.UnlockIfAutoEnabled(ctx) + if err != nil { + return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err) + } + + opPrune := &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + } + op.Op = opPrune + + liveID, writer, err := runner.LogrefWriter() + if err != nil { + return fmt.Errorf("create logref writer: %w", err) + } + defer writer.Close() + opPrune.OperationPrune.OutputLogref = liveID + + if err := runner.UpdateOperation(op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + err = repo.Prune(ctx, writer) + if err != nil { + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_PRUNE_ERROR, + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Error: err.Error(), + }) + + return fmt.Errorf("prune: %w", err) + } + + if err := writer.Close(); err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + + // Run a stats task after a successful prune + if err := runner.ScheduleTask(NewStatsTask(t.Repo(), PlanForSystemTasks, false), TaskPriorityStats); err != nil { + zap.L().Error("schedule stats task", zap.Error(err)) + } + + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_PRUNE_SUCCESS, + }, HookVars{}); err != nil { + return fmt.Errorf("execute prune end hooks: %w", err) + } + + return nil +} diff --git a/internal/orchestrator/tasks/taskrestore.go b/internal/orchestrator/tasks/taskrestore.go new file mode 100644 index 000000000..c74f13c86 --- /dev/null +++ b/internal/orchestrator/tasks/taskrestore.go @@ -0,0 +1,94 @@ +package tasks + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" +) + +func NewOneoffRestoreTask(repo *v1.Repo, planID string, flowID int64, at time.Time, snapshotID, path, target string) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "restore", + TaskName: fmt.Sprintf("restore snapshot %q in repo %q", snapshotID, repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + FlowID: flowID, + RunAt: at, + ProtoOp: &v1.Operation{ + SnapshotId: snapshotID, + Op: &v1.Operation_OperationRestore{ + OperationRestore: &v1.OperationRestore{ + Path: path, + Target: target, + }, + }, + }, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + if err := restoreHelper(ctx, st, taskRunner, snapshotID, path, target); err != nil { + taskRunner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Task: st.Task.Name(), + Error: err.Error(), + }) + return err + } + return nil + }, + } +} + +func restoreHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, snapshotID, path, target string) error { + t := st.Task + op := st.Op + + if snapshotID == "" || path == "" || target == "" { + return errors.New("snapshotID, path, and target are required") + } + + restoreOp := st.Op.GetOperationRestore() + if restoreOp == nil { + return errors.New("operation is not a restore operation") + } + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("couldn't get repo %q: %w", t.RepoID(), err) + } + + var sendWg sync.WaitGroup + lastSent := time.Now() // debounce progress updates, these can endup being very frequent. + summary, err := repo.Restore(ctx, snapshotID, path, target, func(entry *v1.RestoreProgressEntry) { + sendWg.Wait() + if time.Since(lastSent) < 1*time.Second { + return + } + lastSent = time.Now() + + restoreOp.LastStatus = entry + + sendWg.Add(1) + go func() { + if err := taskRunner.UpdateOperation(op); err != nil { + zap.S().Errorf("failed to update oplog with progress for restore: %v", err) + } + sendWg.Done() + }() + }) + + if err != nil { + return err + } + restoreOp.LastStatus = summary + + return nil +} diff --git a/internal/orchestrator/tasks/taskruncommand.go b/internal/orchestrator/tasks/taskruncommand.go new file mode 100644 index 000000000..3a5c0665d --- /dev/null +++ b/internal/orchestrator/tasks/taskruncommand.go @@ -0,0 +1,76 @@ +package tasks + +import ( + "context" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/ioutil" +) + +func NewOneoffRunCommandTask(repo *v1.Repo, planID string, flowID int64, at time.Time, command string) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "run_command", + TaskName: fmt.Sprintf("run command in repo %q", repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + FlowID: flowID, + RunAt: at, + ProtoOp: &v1.Operation{ + Op: &v1.Operation_OperationRunCommand{ + OperationRunCommand: &v1.OperationRunCommand{ + Command: command, + }, + }, + }, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + op := st.Op + rc := op.GetOperationRunCommand() + if rc == nil { + panic("run command task with non-forget operation") + } + + return runCommandHelper(ctx, st, taskRunner, command) + }, + } +} + +func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, command string) error { + t := st.Task + runCmdOp := st.Op.GetOperationRunCommand() + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("get repo %q: %w", t.RepoID(), err) + } + + id, writer, err := taskRunner.LogrefWriter() + if err != nil { + return fmt.Errorf("get logref writer: %w", err) + } + defer writer.Close() + sizeWriter := &ioutil.SizeTrackingWriter{Writer: writer} + defer func() { + runCmdOp.OutputSizeBytes = int64(sizeWriter.Size()) + }() + + runCmdOp.OutputLogref = id + if err := taskRunner.UpdateOperation(st.Op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + if err := repo.RunCommand(ctx, command, sizeWriter); err != nil { + return fmt.Errorf("command %q: %w", command, err) + } + + if err := writer.Close(); err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + + return err +} diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go new file mode 100644 index 000000000..948fc331b --- /dev/null +++ b/internal/orchestrator/tasks/taskstats.go @@ -0,0 +1,111 @@ +package tasks + +import ( + "context" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" +) + +type StatsTask struct { + BaseTask + force bool + didRun bool +} + +func NewStatsTask(repo *v1.Repo, planID string, force bool) Task { + return &StatsTask{ + BaseTask: BaseTask{ + TaskType: "stats", + TaskName: fmt.Sprintf("stats for repo %q", repo.Id), + TaskRepo: repo, + TaskPlanID: planID, + }, + force: force, + } +} + +func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) { + if t.force { + if t.didRun { + return NeverScheduledTask, nil + } + t.didRun = true + return ScheduledTask{ + Task: t, + RunAt: now, + Op: &v1.Operation{ + Op: &v1.Operation_OperationStats{}, + }, + }, nil + } + + // check last stats time + var lastRan time.Time + if err := runner.QueryOperations(oplog.Query{}. + SetInstanceID(runner.InstanceID()). // note: this means that stats tasks run by remote instances are ignored. + SetRepoGUID(t.Repo().GetGuid()). + SetReversed(true), func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationStats); ok && op.UnixTimeEndMs != 0 { + lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) + return oplog.ErrStopIteration + } + return nil + }); err != nil { + return NeverScheduledTask, fmt.Errorf("finding last check run time: %w", err) + } + + // Runs at most once per day. + if time.Since(lastRan) < 24*time.Hour { + return NeverScheduledTask, nil + } + return ScheduledTask{ + Task: t, + RunAt: now, + Op: &v1.Operation{ + Op: &v1.Operation_OperationStats{}, + }, + }, nil +} + +func (t *StatsTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error { + if err := statsHelper(ctx, st, runner); err != nil { + runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_ANY_ERROR, + }, HookVars{ + Task: st.Task.Name(), + Error: err.Error(), + }) + return err + } + + return nil +} + +func statsHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + t := st.Task + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("get repo %q: %w", t.RepoID(), err) + } + + stats, err := repo.Stats(ctx) + if err != nil { + return fmt.Errorf("get stats: %w", err) + } + + op := st.Op + op.Op = &v1.Operation_OperationStats{ + OperationStats: &v1.OperationStats{ + Stats: stats, + }, + } + + return nil +} diff --git a/internal/protoutil/conditions.go b/internal/protoutil/conditions.go new file mode 100644 index 000000000..8c6aa6e3b --- /dev/null +++ b/internal/protoutil/conditions.go @@ -0,0 +1,52 @@ +package protoutil + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +var startConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_CHECK_START: true, + v1.Hook_CONDITION_PRUNE_START: true, + v1.Hook_CONDITION_SNAPSHOT_START: true, + v1.Hook_CONDITION_FORGET_START: true, +} + +var errorConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_ANY_ERROR: true, + v1.Hook_CONDITION_CHECK_ERROR: true, + v1.Hook_CONDITION_PRUNE_ERROR: true, + v1.Hook_CONDITION_SNAPSHOT_ERROR: true, + v1.Hook_CONDITION_FORGET_ERROR: true, + v1.Hook_CONDITION_UNKNOWN: true, +} + +var logConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_SNAPSHOT_END: true, +} + +var successConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_CHECK_SUCCESS: true, + v1.Hook_CONDITION_PRUNE_SUCCESS: true, + v1.Hook_CONDITION_SNAPSHOT_SUCCESS: true, + v1.Hook_CONDITION_FORGET_SUCCESS: true, +} + +// IsErrorCondition returns true if the event is an error condition. +func IsErrorCondition(event v1.Hook_Condition) bool { + return errorConditionsMap[event] +} + +// IsLogCondition returns true if the event is a log condition. +func IsLogCondition(event v1.Hook_Condition) bool { + return logConditionsMap[event] +} + +// IsStartCondition returns true if the event is a start condition. +func IsStartCondition(event v1.Hook_Condition) bool { + return startConditionsMap[event] +} + +// IsSuccessCondition returns true if the event is a success condition. +func IsSuccessCondition(event v1.Hook_Condition) bool { + return successConditionsMap[event] +} diff --git a/internal/protoutil/conditions_test.go b/internal/protoutil/conditions_test.go new file mode 100644 index 000000000..fb814609f --- /dev/null +++ b/internal/protoutil/conditions_test.go @@ -0,0 +1,73 @@ +package protoutil + +import ( + "strings" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func TestStartConditionsMap(t *testing.T) { + // Test that all conditions with "_START" in their name are correctly identified by IsStartCondition + for cond := range v1.Hook_Condition_name { + condEnum := v1.Hook_Condition(cond) + condName := condEnum.String() + if strings.Contains(condName, "_START") { + if !IsStartCondition(condEnum) { + t.Errorf("Condition %s contains '_START' but IsStartCondition returned false", condName) + } + } else { + if IsStartCondition(condEnum) { + t.Errorf("Condition %s does not contain '_START' but IsStartCondition returned true", condName) + } + } + } +} + +func TestErrorConditionsMap(t *testing.T) { + // Special case for CONDITION_UNKNOWN which should be identified as an error condition + if !IsErrorCondition(v1.Hook_CONDITION_UNKNOWN) { + t.Errorf("CONDITION_UNKNOWN should be identified as an error condition") + } + + // Special case for ANY_ERROR which should be identified as an error condition + if !IsErrorCondition(v1.Hook_CONDITION_ANY_ERROR) { + t.Errorf("CONDITION_ANY_ERROR should be identified as an error condition") + } + + // Test that all conditions with "_ERROR" in their name are correctly identified by IsErrorCondition + for cond := range v1.Hook_Condition_name { + condEnum := v1.Hook_Condition(cond) + condName := condEnum.String() + + // Skip the special cases we already checked + if condEnum == v1.Hook_CONDITION_UNKNOWN || condEnum == v1.Hook_CONDITION_ANY_ERROR { + continue + } + + if strings.Contains(condName, "_ERROR") { + if !IsErrorCondition(condEnum) { + t.Errorf("Condition %s contains '_ERROR' but IsErrorCondition returned false", condName) + } + } else if IsErrorCondition(condEnum) { + t.Errorf("Condition %s does not contain '_ERROR' but IsErrorCondition returned true", condName) + } + } +} + +func TestSuccessConditionsMap(t *testing.T) { + // Test that all conditions with "_SUCCESS" in their name are correctly identified by IsSuccessCondition + for cond := range v1.Hook_Condition_name { + condEnum := v1.Hook_Condition(cond) + condName := condEnum.String() + if strings.Contains(condName, "_SUCCESS") { + if !IsSuccessCondition(condEnum) { + t.Errorf("Condition %s contains '_SUCCESS' but IsSuccessCondition returned false", condName) + } + } else { + if IsSuccessCondition(condEnum) { + t.Errorf("Condition %s does not contain '_SUCCESS' but IsSuccessCondition returned true", condName) + } + } + } +} diff --git a/internal/protoutil/conversion.go b/internal/protoutil/conversion.go new file mode 100644 index 000000000..3d81d60e4 --- /dev/null +++ b/internal/protoutil/conversion.go @@ -0,0 +1,144 @@ +package protoutil + +import ( + "errors" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/pkg/restic" +) + +func SnapshotToProto(s *restic.Snapshot) *v1.ResticSnapshot { + return &v1.ResticSnapshot{ + Id: s.Id, + UnixTimeMs: s.UnixTimeMs(), + Tree: s.Tree, + Paths: s.Paths, + Hostname: s.Hostname, + Username: s.Username, + Tags: s.Tags, + Parent: s.Parent, + Summary: &v1.SnapshotSummary{ + FilesNew: int64(s.SnapshotSummary.FilesNew), + FilesChanged: int64(s.SnapshotSummary.FilesChanged), + FilesUnmodified: int64(s.SnapshotSummary.FilesUnmodified), + DirsNew: int64(s.SnapshotSummary.DirsNew), + DirsChanged: int64(s.SnapshotSummary.DirsChanged), + DirsUnmodified: int64(s.SnapshotSummary.DirsUnmodified), + DataBlobs: int64(s.SnapshotSummary.DataBlobs), + TreeBlobs: int64(s.SnapshotSummary.TreeBlobs), + DataAdded: int64(s.SnapshotSummary.DataAdded), + TotalFilesProcessed: int64(s.SnapshotSummary.TotalFilesProcessed), + TotalBytesProcessed: int64(s.SnapshotSummary.TotalBytesProcessed), + TotalDuration: float64(s.SnapshotSummary.DurationMs()) / 1000.0, + }, + } +} + +func LsEntryToProto(e *restic.LsEntry) *v1.LsEntry { + return &v1.LsEntry{ + Name: e.Name, + Type: e.Type, + Path: e.Path, + Uid: int64(e.Uid), + Gid: int64(e.Gid), + Size: int64(e.Size), + Mode: int64(e.Mode), + Mtime: e.Mtime, + Atime: e.Atime, + Ctime: e.Ctime, + } +} + +func BackupProgressEntryToProto(b *restic.BackupProgressEntry) *v1.BackupProgressEntry { + switch b.MessageType { + case "summary": + return &v1.BackupProgressEntry{ + Entry: &v1.BackupProgressEntry_Summary{ + Summary: &v1.BackupProgressSummary{ + FilesNew: int64(b.FilesNew), + FilesChanged: int64(b.FilesChanged), + FilesUnmodified: int64(b.FilesUnmodified), + DirsNew: int64(b.DirsNew), + DirsChanged: int64(b.DirsChanged), + DirsUnmodified: int64(b.DirsUnmodified), + DataBlobs: int64(b.DataBlobs), + TreeBlobs: int64(b.TreeBlobs), + DataAdded: int64(b.DataAdded), + TotalFilesProcessed: int64(b.TotalFilesProcessed), + TotalBytesProcessed: int64(b.TotalBytesProcessed), + TotalDuration: float64(b.TotalDuration), + SnapshotId: b.SnapshotId, + }, + }, + } + case "status": + return &v1.BackupProgressEntry{ + Entry: &v1.BackupProgressEntry_Status{ + Status: &v1.BackupProgressStatusEntry{ + PercentDone: b.PercentDone, + TotalFiles: int64(b.TotalFiles), + FilesDone: int64(b.FilesDone), + TotalBytes: int64(b.TotalBytes), + BytesDone: int64(b.BytesDone), + CurrentFile: b.CurrentFiles, + }, + }, + } + default: + return nil + } +} + +// BackupProgressEntryToBackupError converts a BackupProgressEntry to a BackupError if it's type is "error" +func BackupProgressEntryToBackupError(b *restic.BackupProgressEntry) (*v1.BackupProgressError, error) { + if b.MessageType != "error" { + return nil, errors.New("BackupProgressEntry is not of type error") + } + + return &v1.BackupProgressError{ + Item: b.Item, + During: b.During, + }, nil +} + +func RetentionPolicyFromProto(p *v1.RetentionPolicy) *restic.RetentionPolicy { + switch p := p.GetPolicy().(type) { + case *v1.RetentionPolicy_PolicyKeepAll: + return nil + case *v1.RetentionPolicy_PolicyTimeBucketed: + return &restic.RetentionPolicy{ + KeepDaily: int(p.PolicyTimeBucketed.Daily), + KeepHourly: int(p.PolicyTimeBucketed.Hourly), + KeepWeekly: int(p.PolicyTimeBucketed.Weekly), + KeepMonthly: int(p.PolicyTimeBucketed.Monthly), + KeepYearly: int(p.PolicyTimeBucketed.Yearly), + } + case *v1.RetentionPolicy_PolicyKeepLastN: + return &restic.RetentionPolicy{ + KeepLastN: int(p.PolicyKeepLastN), + } + default: + return nil + } +} + +func RestoreProgressEntryToProto(p *restic.RestoreProgressEntry) *v1.RestoreProgressEntry { + return &v1.RestoreProgressEntry{ + MessageType: p.MessageType, + TotalFiles: int64(p.TotalFiles), + FilesRestored: int64(p.FilesRestored), + TotalBytes: int64(p.TotalBytes), + BytesRestored: int64(p.BytesRestored), + PercentDone: p.PercentDone, + } +} + +func RepoStatsToProto(s *restic.RepoStats) *v1.RepoStats { + return &v1.RepoStats{ + TotalSize: int64(s.TotalSize), + TotalUncompressedSize: int64(s.TotalUncompressedSize), + CompressionRatio: s.CompressionRatio, + TotalBlobCount: int64(s.TotalBlobCount), + SnapshotCount: int64(s.SnapshotsCount), + } +} diff --git a/internal/protoutil/conversion_test.go b/internal/protoutil/conversion_test.go new file mode 100644 index 000000000..9274c67dc --- /dev/null +++ b/internal/protoutil/conversion_test.go @@ -0,0 +1,189 @@ +package protoutil + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/pkg/restic" + "google.golang.org/protobuf/proto" +) + +func TestSnapshotToProto(t *testing.T) { + snapshot := &restic.Snapshot{ + Id: "db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52", + Time: "2023-11-10T19:14:17.053824063-08:00", + Tree: "3e2918b261948e69602ee9504b8f475bcc7cdc4dcec0b3f34ecdb014287d07b2", + Paths: []string{"/backrest"}, + Hostname: "pop-os", + Username: "dontpanic", + Tags: []string{}, + Parent: "", + SnapshotSummary: restic.SnapshotSummary{ + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + BackupStart: "2023-11-10T19:14:17.053824063-08:00", + BackupEnd: "2023-11-10T19:15:17.053824063-08:00", + }, + } + + want := &v1.ResticSnapshot{ + Id: "db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52", + UnixTimeMs: 1699672457053, + Tree: "3e2918b261948e69602ee9504b8f475bcc7cdc4dcec0b3f34ecdb014287d07b2", + Paths: []string{"/backrest"}, + Hostname: "pop-os", + Username: "dontpanic", + Tags: []string{}, + Parent: "", + Summary: &v1.SnapshotSummary{ + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + TotalDuration: 60.0, + }, + } + + got := SnapshotToProto(snapshot) + + if !proto.Equal(want, got) { + t.Errorf("wanted %+v, got: %+v", want, got) + } +} + +func TestBackupProgressEntryToProto(t *testing.T) { + cases := []struct { + name string + entry *restic.BackupProgressEntry + want *v1.BackupProgressEntry + }{ + { + name: "summary", + entry: &restic.BackupProgressEntry{ + MessageType: "summary", + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + TotalDuration: 12.0, + SnapshotId: "db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52", + PercentDone: 13.0, // should be ignored. + }, + want: &v1.BackupProgressEntry{ + Entry: &v1.BackupProgressEntry_Summary{ + Summary: &v1.BackupProgressSummary{ + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + TotalDuration: 12.0, + SnapshotId: "db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52", + }, + }, + }, + }, + { + name: "status", + entry: &restic.BackupProgressEntry{ + MessageType: "status", + PercentDone: 13.0, + TotalFiles: 14, + FilesDone: 15, + TotalBytes: 16, + BytesDone: 17, + }, + want: &v1.BackupProgressEntry{ + Entry: &v1.BackupProgressEntry_Status{ + Status: &v1.BackupProgressStatusEntry{ + PercentDone: 13.0, + TotalFiles: 14, + FilesDone: 15, + TotalBytes: 16, + BytesDone: 17, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := BackupProgressEntryToProto(c.entry) + if !proto.Equal(got, c.want) { + t.Errorf("wanted: %+v, got: %+v", c.want, got) + } + }) + } +} + +func TestRepoStatsToProto(t *testing.T) { + cases := []struct { + name string + stats *restic.RepoStats + want *v1.RepoStats + }{ + { + name: "no stats", + stats: &restic.RepoStats{}, + want: &v1.RepoStats{}, + }, + { + name: "with stats", + stats: &restic.RepoStats{ + TotalSize: 1, + TotalUncompressedSize: 2, + CompressionRatio: 3, + TotalBlobCount: 5, + CompressionProgress: 6, + CompressionSpaceSaving: 7, + SnapshotsCount: 8, + }, + want: &v1.RepoStats{ + TotalSize: 1, + TotalUncompressedSize: 2, + CompressionRatio: 3, + TotalBlobCount: 5, + SnapshotCount: 8, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := RepoStatsToProto(c.stats) + if !proto.Equal(got, c.want) { + t.Errorf("wanted: %+v, got: %+v", c.want, got) + } + }) + } +} diff --git a/internal/protoutil/opselector.go b/internal/protoutil/opselector.go new file mode 100644 index 000000000..fdb48468b --- /dev/null +++ b/internal/protoutil/opselector.go @@ -0,0 +1,28 @@ +package protoutil + +import ( + "errors" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" +) + +func OpSelectorToQuery(sel *v1.OpSelector) (oplog.Query, error) { + if sel == nil { + return oplog.Query{}, errors.New("empty selector") + } + + q := oplog.Query{ + RepoGUID: sel.RepoGuid, + PlanID: sel.PlanId, + SnapshotID: sel.SnapshotId, + FlowID: sel.FlowId, + InstanceID: sel.InstanceId, + } + if len(sel.Ids) > 0 && !reflect.DeepEqual(q, oplog.Query{}) { + return oplog.Query{}, errors.New("cannot specify both query and ids") + } + q.OpIDs = sel.Ids + return q, nil +} diff --git a/internal/protoutil/schedule.go b/internal/protoutil/schedule.go new file mode 100644 index 000000000..f6155822b --- /dev/null +++ b/internal/protoutil/schedule.go @@ -0,0 +1,75 @@ +package protoutil + +import ( + "errors" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/gitploy-io/cronexpr" +) + +var ErrScheduleDisabled = errors.New("never") + +// ResolveSchedule resolves a schedule to the next time it should run based on last execution. +// note that this is different from backup behavior which is always relative to the current time. +func ResolveSchedule(sched *v1.Schedule, lastRan time.Time, curTime time.Time) (time.Time, error) { + var t time.Time + switch sched.GetClock() { + case v1.Schedule_CLOCK_DEFAULT, v1.Schedule_CLOCK_LOCAL: + t = curTime.Local() + case v1.Schedule_CLOCK_UTC: + t = curTime.UTC() + case v1.Schedule_CLOCK_LAST_RUN_TIME: + t = lastRan + default: + return time.Time{}, fmt.Errorf("unknown clock type: %v", sched.GetClock().String()) + } + + switch s := sched.GetSchedule().(type) { + case *v1.Schedule_Disabled, nil: + return time.Time{}, ErrScheduleDisabled + case *v1.Schedule_MaxFrequencyDays: + return t.Add(time.Duration(s.MaxFrequencyDays) * 24 * time.Hour), nil + case *v1.Schedule_MaxFrequencyHours: + return t.Add(time.Duration(s.MaxFrequencyHours) * time.Hour), nil + case *v1.Schedule_Cron: + cron, err := cronexpr.ParseInLocation(s.Cron, time.Now().Location().String()) + if err != nil { + return time.Time{}, fmt.Errorf("parse cron %q: %w", s.Cron, err) + } + return cron.Next(t), nil + default: + return time.Time{}, fmt.Errorf("unknown schedule type: %T", s) + } +} + +func ValidateSchedule(sched *v1.Schedule) error { + switch s := sched.GetSchedule().(type) { + case *v1.Schedule_MaxFrequencyDays: + if s.MaxFrequencyDays < 1 { + return errors.New("invalid max frequency days") + } + case *v1.Schedule_MaxFrequencyHours: + if s.MaxFrequencyHours < 1 { + return errors.New("invalid max frequency hours") + } + case *v1.Schedule_Cron: + if s.Cron == "" { + return errors.New("empty cron expression") + } + _, err := cronexpr.ParseInLocation(s.Cron, time.Now().Location().String()) + if err != nil { + return fmt.Errorf("invalid cron %q: %w", s.Cron, err) + } + case nil: + return nil + case *v1.Schedule_Disabled: + if !s.Disabled { + return errors.New("disabled boolean must be set to true") + } + default: + return fmt.Errorf("unknown schedule type: %T", s) + } + return nil +} diff --git a/internal/protoutil/syncconversion.go b/internal/protoutil/syncconversion.go new file mode 100644 index 000000000..7a6f6b0a5 --- /dev/null +++ b/internal/protoutil/syncconversion.go @@ -0,0 +1,33 @@ +package protoutil + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func RepoToRemoteRepo(r *v1.Repo) *v1.RemoteRepo { + if r == nil { + return nil + } + return &v1.RemoteRepo{ + Id: r.Id, + Guid: r.Guid, + Uri: r.Uri, + Password: r.Password, + Env: r.Env, + Flags: r.Flags, + } +} + +func RemoteRepoToRepo(r *v1.RemoteRepo) *v1.Repo { + if r == nil { + return nil + } + return &v1.Repo{ + Id: r.Id, + Guid: r.Guid, + Uri: r.Uri, + Password: r.Password, + Env: r.Env, + Flags: r.Flags, + } +} diff --git a/internal/protoutil/syncconversion_test.go b/internal/protoutil/syncconversion_test.go new file mode 100644 index 000000000..e4f8f0827 --- /dev/null +++ b/internal/protoutil/syncconversion_test.go @@ -0,0 +1,96 @@ +package protoutil + +import ( + "reflect" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func TestRepoToRemoteRepo(t *testing.T) { + tests := []struct { + name string + repo *v1.Repo + want *v1.RemoteRepo + }{ + { + name: "basic conversion", + repo: &v1.Repo{ + Id: "1", + Uri: "http://example.com", + Password: "password", + Env: []string{"FOO=BAR"}, + Flags: []string{"flag1", "flag2"}, + }, + want: &v1.RemoteRepo{ + Id: "1", + Uri: "http://example.com", + Password: "password", + Env: []string{"FOO=BAR"}, + Flags: []string{"flag1", "flag2"}, + }, + }, + { + name: "empty repo", + repo: &v1.Repo{}, + want: &v1.RemoteRepo{}, + }, + { + name: "nil repo", + repo: nil, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RepoToRemoteRepo(tt.repo); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RepoToRemoteRepo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRemoteRepoToRepo(t *testing.T) { + tests := []struct { + name string + remoteRepo *v1.RemoteRepo + want *v1.Repo + }{ + { + name: "basic conversion", + remoteRepo: &v1.RemoteRepo{ + Id: "1", + Uri: "http://example.com", + Password: "password", + Env: []string{"FOO=BAR"}, + Flags: []string{"flag1", "flag2"}, + }, + want: &v1.Repo{ + Id: "1", + Uri: "http://example.com", + Password: "password", + Env: []string{"FOO=BAR"}, + Flags: []string{"flag1", "flag2"}, + }, + }, + { + name: "empty remote repo", + remoteRepo: &v1.RemoteRepo{}, + want: &v1.Repo{}, + }, + { + name: "nil remote repo", + remoteRepo: nil, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RemoteRepoToRepo(tt.remoteRepo); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RemoteRepoToRepo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/protoutil/validation.go b/internal/protoutil/validation.go new file mode 100644 index 000000000..feea127ad --- /dev/null +++ b/internal/protoutil/validation.go @@ -0,0 +1,64 @@ +package protoutil + +import ( + "errors" + "fmt" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/pkg/restic" +) + +var ( + errIDRequired = errors.New("id is required") + errFlowIDRequired = errors.New("flow_id is required") + errRepoIDRequired = errors.New("repo_id is required") + errRepoGUIDRequired = errors.New("repo_guid is required") + errPlanIDRequired = errors.New("plan_id is required") + errInstanceIDRequired = errors.New("instance_id is required") + errUnixTimeStartMsRequired = errors.New("unix_time_start_ms must be non-zero") +) + +// ValidateOperation verifies critical properties of the operation proto. +func ValidateOperation(op *v1.Operation) error { + if op.Id == 0 { + return errIDRequired + } + if op.RepoGuid == "" { + return errRepoGUIDRequired + } + if op.FlowId == 0 { + return errFlowIDRequired + } + if op.RepoId == "" { + return errRepoIDRequired + } + if op.PlanId == "" { + return errPlanIDRequired + } + if op.InstanceId == "" { + return errInstanceIDRequired + } + if op.UnixTimeStartMs == 0 { + return errUnixTimeStartMsRequired + } + if op.SnapshotId != "" { + if err := restic.ValidateSnapshotId(op.SnapshotId); err != nil { + return fmt.Errorf("operation.snapshot_id is invalid: %w", err) + } + } + return nil +} + +// ValidateSnapshot verifies critical properties of the snapshot proto representation. +func ValidateSnapshot(s *v1.ResticSnapshot) error { + if s.Id == "" { + return errors.New("snapshot.id is required") + } + if s.UnixTimeMs == 0 { + return errors.New("snapshot.unix_time_ms must be non-zero") + } + if err := restic.ValidateSnapshotId(s.Id); err != nil { + return err + } + return nil +} diff --git a/internal/queue/genheap.go b/internal/queue/genheap.go new file mode 100644 index 000000000..4e46e2e6e --- /dev/null +++ b/internal/queue/genheap.go @@ -0,0 +1,42 @@ +package queue + +// genericHeap is a generic heap implementation that can be used with any type that satisfies the constraints.Ordered interface. +type GenericHeap[T Comparable[T]] []T + +func (h GenericHeap[T]) Len() int { + return len(h) +} + +func (h GenericHeap[T]) Swap(i, j int) { + h[i], h[j] = h[j], h[i] +} + +// Push pushes an element onto the heap. Do not call directly, use heap.Push +func (h *GenericHeap[T]) Push(x interface{}) { + *h = append(*h, x.(T)) +} + +// Pop pops an element from the heap. Do not call directly, use heap.Pop +func (h *GenericHeap[T]) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} + +func (h GenericHeap[T]) Peek() T { + if len(h) == 0 { + var zero T + return zero + } + return h[0] +} + +func (h GenericHeap[T]) Less(i, j int) bool { + return h[i].Less(h[j]) +} + +type Comparable[T any] interface { + Less(other T) bool +} diff --git a/internal/queue/genheap_test.go b/internal/queue/genheap_test.go new file mode 100644 index 000000000..e7167a62e --- /dev/null +++ b/internal/queue/genheap_test.go @@ -0,0 +1,54 @@ +package queue + +import ( + "container/heap" + "testing" +) + +type val struct { + v int +} + +func (v val) Less(other val) bool { + return v.v < other.v +} + +func (v val) Eq(other val) bool { + return v.v == other.v +} + +func TestGenericHeapInit(t *testing.T) { + t.Parallel() + genHeap := GenericHeap[val]{{v: 3}, {v: 2}, {v: 1}} + heap.Init(&genHeap) + + if genHeap.Len() != 3 { + t.Errorf("expected length to be 3, got %d", genHeap.Len()) + } + + for _, i := range []int{1, 2, 3} { + v := heap.Pop(&genHeap).(val) + if v.v != i { + t.Errorf("expected %d, got %d", i, v.v) + } + } +} + +func TestGenericHeapPushPop(t *testing.T) { + t.Parallel() + genHeap := GenericHeap[val]{} // empty heap + heap.Push(&genHeap, val{v: 3}) + heap.Push(&genHeap, val{v: 2}) + heap.Push(&genHeap, val{v: 1}) + + if genHeap.Len() != 3 { + t.Errorf("expected length to be 3, got %d", genHeap.Len()) + } + + for _, i := range []int{1, 2, 3} { + v := heap.Pop(&genHeap).(val) + if v.v != i { + t.Errorf("expected %d, got %d", i, v.v) + } + } +} diff --git a/internal/queue/timepriorityqueue.go b/internal/queue/timepriorityqueue.go new file mode 100644 index 000000000..a3f60fe7c --- /dev/null +++ b/internal/queue/timepriorityqueue.go @@ -0,0 +1,127 @@ +package queue + +import ( + "container/heap" + "context" + "time" +) + +// TimePriorityQueue is a priority queue that dequeues elements at (or after) a specified time, and prioritizes elements based on a priority value. It is safe for concurrent use. +type TimePriorityQueue[T equals[T]] struct { + tqueue TimeQueue[priorityEntry[T]] + ready GenericHeap[priorityEntry[T]] +} + +func NewTimePriorityQueue[T equals[T]]() *TimePriorityQueue[T] { + return &TimePriorityQueue[T]{ + tqueue: TimeQueue[priorityEntry[T]]{}, + ready: GenericHeap[priorityEntry[T]]{}, + } +} + +func (t *TimePriorityQueue[T]) Len() int { + t.tqueue.mu.Lock() + defer t.tqueue.mu.Unlock() + return t.tqueue.heap.Len() + t.ready.Len() +} + +func (t *TimePriorityQueue[T]) Peek() T { + t.tqueue.mu.Lock() + defer t.tqueue.mu.Unlock() + + if t.ready.Len() > 0 { + return t.ready.Peek().v + } + if t.tqueue.heap.Len() > 0 { + return t.tqueue.heap.Peek().v.v + } + var zero T + return zero +} + +func (t *TimePriorityQueue[T]) Reset() []T { + t.tqueue.mu.Lock() + defer t.tqueue.mu.Unlock() + var res []T + for t.ready.Len() > 0 { + res = append(res, heap.Pop(&t.ready).(priorityEntry[T]).v) + } + for t.tqueue.heap.Len() > 0 { + res = append(res, heap.Pop(&t.tqueue.heap).(timeQueueEntry[priorityEntry[T]]).v.v) + } + return res +} + +func (t *TimePriorityQueue[T]) GetAll() []T { + t.tqueue.mu.Lock() + defer t.tqueue.mu.Unlock() + res := make([]T, 0, t.tqueue.heap.Len()+t.ready.Len()) + for _, entry := range t.tqueue.heap { + res = append(res, entry.v.v) + } + for _, entry := range t.ready { + res = append(res, entry.v) + } + return res +} + +func (t *TimePriorityQueue[T]) Remove(v T) { + t.tqueue.mu.Lock() + defer t.tqueue.mu.Unlock() + + for idx := 0; idx < t.tqueue.heap.Len(); idx++ { + if t.tqueue.heap[idx].v.v.Eq(v) { + heap.Remove(&t.tqueue.heap, idx) + return + } + } + + for idx := 0; idx < t.ready.Len(); idx++ { + if t.ready[idx].v.Eq(v) { + heap.Remove(&t.ready, idx) + return + } + } +} + +func (t *TimePriorityQueue[T]) Enqueue(at time.Time, priority int, v T) { + t.tqueue.Enqueue(at, priorityEntry[T]{at, priority, v}) +} + +func (t *TimePriorityQueue[T]) Dequeue(ctx context.Context) T { + t.tqueue.mu.Lock() + for { + for t.tqueue.heap.Len() > 0 { + thead := t.tqueue.heap.Peek() // peek at the head of the time queue + if thead.at.Before(time.Now()) { + tqe := heap.Pop(&t.tqueue.heap).(timeQueueEntry[priorityEntry[T]]) + heap.Push(&t.ready, tqe.v) + } else { + break + } + } + if t.ready.Len() > 0 { + defer t.tqueue.mu.Unlock() + return heap.Pop(&t.ready).(priorityEntry[T]).v + } + t.tqueue.mu.Unlock() + // wait for the next element to be ready + val := t.tqueue.Dequeue(ctx) + t.tqueue.mu.Lock() + heap.Push(&t.ready, val) + } +} + +type priorityEntry[T equals[T]] struct { + at time.Time + priority int + v T +} + +func (t priorityEntry[T]) Less(other priorityEntry[T]) bool { + return t.priority > other.priority +} + +func (t priorityEntry[T]) Eq(other priorityEntry[T]) bool { + return t.at == other.at && t.priority == other.priority && t.v.Eq(other.v) +} diff --git a/internal/queue/timepriorityqueue_test.go b/internal/queue/timepriorityqueue_test.go new file mode 100644 index 000000000..c3850e2be --- /dev/null +++ b/internal/queue/timepriorityqueue_test.go @@ -0,0 +1,200 @@ +package queue + +import ( + "context" + "math/rand" + "slices" + "testing" + "time" +) + +// TestTPQEnqueue tests that enqueued elements are retruned highest priority first. +func TestTPQPriority(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + + now := time.Now().Add(-time.Second) + for i := 0; i < 100; i++ { + tpq.Enqueue(now, i, val{i}) + } + + if tpq.Len() != 100 { + t.Errorf("expected length to be 100, got %d", tpq.Len()) + } + + for i := 99; i >= 0; i-- { + v := tpq.Dequeue(context.Background()) + if v.v != i { + t.Errorf("expected %d, got %d", i, v) + } + } +} + +func TestTPQMixedReadinessStates(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + + now := time.Now() + for i := 0; i < 100; i++ { + tpq.Enqueue(now.Add(-100*time.Millisecond), i, val{i}) + } + for i := 0; i < 100; i++ { + tpq.Enqueue(now.Add(100*time.Millisecond), i, val{i}) + } + + if tpq.Len() != 200 { + t.Errorf("expected length to be 100, got %d", tpq.Len()) + } + + for j := 0; j < 2; j++ { + for i := 99; i >= 0; i-- { + v := tpq.Dequeue(context.Background()) + if v.v != i { + t.Errorf("pass %d expected %d, got %d", j, i, v) + } + } + } +} + +func TestTPQStress(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + start := time.Now() + + totalEnqueued := 0 + totalEnqueuedSum := 0 + + go func() { + ctx, _ := context.WithDeadline(context.Background(), start.Add(1*time.Second)) + for ctx.Err() == nil { + v := rand.Intn(100) + 1 + tpq.Enqueue(time.Now().Add(time.Duration(rand.Intn(1000)-500)*time.Millisecond), rand.Intn(5), val{v}) + totalEnqueuedSum += v + totalEnqueued++ + } + }() + + ctx, _ := context.WithDeadline(context.Background(), start.Add(3*time.Second)) + totalDequeued := 0 + sum := 0 + for ctx.Err() == nil || totalDequeued < totalEnqueued { + v := tpq.Dequeue(ctx) + if v.v != 0 { + totalDequeued++ + sum += v.v + } + } + + if totalDequeued != totalEnqueued { + t.Errorf("expected totalDequeued to be %d, got %d", totalEnqueued, totalDequeued) + } + + if sum != totalEnqueuedSum { + t.Errorf("expected sum to be %d, got %d", totalEnqueuedSum, sum) + } +} + +func TestTPQRemove(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + + now := time.Now().Add(-time.Second) // make sure the time is in the past + for i := 0; i < 100; i++ { + tpq.Enqueue(now, -i, val{i}) + } + + if tpq.Len() != 100 { + t.Errorf("expected length to be 100, got %d", tpq.Len()) + } + + // remove all even numbers, dequeue the odd numbers + for i := 0; i < 100; i += 2 { + tpq.Remove(val{i}) + v := tpq.Dequeue(context.Background()) + if v.v != i+1 { + t.Errorf("expected %d, got %d", i+1, v) + } + } + + if tpq.Len() != 0 { + t.Errorf("expected length to be 0, got %d", tpq.Len()) + } +} + +func TestTPQReset(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + + now := time.Now() // make sure the time is in the past + for i := 0; i < 50; i++ { + tpq.Enqueue(now.Add(time.Second), i, val{i}) + } + for i := 50; i < 100; i++ { + tpq.Enqueue(now.Add(-time.Second), i, val{i}) + } + + if tpq.Len() != 100 { + t.Errorf("expected length to be 100, got %d", tpq.Len()) + } + + dv := tpq.Dequeue(context.Background()) + if dv.v != 99 { + t.Errorf("expected 99, got %d", dv.v) + } + + vals := tpq.Reset() + + if len(vals) != 99 { + t.Errorf("expected length to be 100, got %d", len(vals)) + } + + slices.SortFunc(vals, func(i, j val) int { + if i.v > j.v { + return 1 + } + return -1 + }) + + for i := 0; i < 99; i++ { + if vals[i].v != i { + t.Errorf("expected %d, got %d", i, vals[i].v) + } + } + + if tpq.Len() != 0 { + t.Errorf("expected length to be 0, got %d", tpq.Len()) + } +} + +func TestTPQGetAll(t *testing.T) { + t.Parallel() + tpq := NewTimePriorityQueue[val]() + now := time.Now() + + for i := 0; i < 100; i++ { + tpq.Enqueue(now.Add(time.Second), i, val{i}) + } + + if tpq.Len() != 100 { + t.Errorf("expected length to be 100, got %d", tpq.Len()) + } + + vals := tpq.GetAll() + + if len(vals) != 100 { + t.Errorf("expected length to be 100, got %d", len(vals)) + } + + slices.SortFunc(vals, func(i, j val) int { + if i.v > j.v { + return 1 + } + return -1 + }) + + for i := 0; i < 100; i++ { + if vals[i].v != i { + t.Errorf("expected %d, got %d", i, vals[i].v) + } + } +} diff --git a/internal/queue/timequeue.go b/internal/queue/timequeue.go new file mode 100644 index 000000000..5674103ba --- /dev/null +++ b/internal/queue/timequeue.go @@ -0,0 +1,160 @@ +package queue + +import ( + "container/heap" + "context" + "sync" + "sync/atomic" + "time" +) + +// TimeQueue is a priority queue that dequeues elements at (or after) a specified time. It is safe for concurrent use. +type TimeQueue[T equals[T]] struct { + heap GenericHeap[timeQueueEntry[T]] + + dequeueMu sync.Mutex + mu sync.Mutex + notify atomic.Pointer[chan struct{}] +} + +func NewTimeQueue[T equals[T]]() *TimeQueue[T] { + return &TimeQueue[T]{ + heap: GenericHeap[timeQueueEntry[T]]{}, + } +} + +func (t *TimeQueue[T]) Enqueue(at time.Time, v T) { + t.mu.Lock() + heap.Push(&t.heap, timeQueueEntry[T]{at, v}) + t.mu.Unlock() + if n := t.notify.Load(); n != nil { + select { + case *n <- struct{}{}: + default: + } + } +} + +func (t *TimeQueue[T]) Len() int { + t.mu.Lock() + defer t.mu.Unlock() + return t.heap.Len() +} + +func (t *TimeQueue[T]) Peek() T { + t.mu.Lock() + defer t.mu.Unlock() + + if t.heap.Len() == 0 { + var zero T + return zero + } + return t.heap.Peek().v +} + +func (t *TimeQueue[T]) Reset() []T { + t.mu.Lock() + defer t.mu.Unlock() + + var res []T + for t.heap.Len() > 0 { + res = append(res, heap.Pop(&t.heap).(timeQueueEntry[T]).v) + } + return res +} + +func (t *TimeQueue[T]) Remove(v T) { + t.mu.Lock() + defer t.mu.Unlock() + + for idx := 0; idx < t.heap.Len(); idx++ { + if t.heap[idx].v.Eq(v) { + heap.Remove(&t.heap, idx) + return + } + } +} + +func (t *TimeQueue[T]) GetAll() []T { + t.mu.Lock() + defer t.mu.Unlock() + + res := make([]T, 0, t.heap.Len()) + for _, entry := range t.heap { + res = append(res, entry.v) + } + return res +} + +func (t *TimeQueue[T]) Dequeue(ctx context.Context) T { + t.dequeueMu.Lock() + defer t.dequeueMu.Unlock() + + notify := make(chan struct{}, 1) + t.notify.Store(¬ify) + defer t.notify.Store(nil) + + for { + t.mu.Lock() + var wait time.Duration + if t.heap.Len() > 0 { + val := t.heap.Peek() + wait = time.Until(val.at) + if wait <= 0 { + defer t.mu.Unlock() + return heap.Pop(&t.heap).(timeQueueEntry[T]).v + } + } + if wait == 0 || wait > 3*time.Minute { + wait = 3 * time.Minute + } + t.mu.Unlock() + + timer := time.NewTimer(wait) + + select { + case <-timer.C: + t.mu.Lock() + if len(t.heap) == 0 { + t.mu.Unlock() + continue + } + val := t.heap.Peek() + if val.at.After(time.Now()) { + t.mu.Unlock() + continue + } + heap.Pop(&t.heap) + t.mu.Unlock() + return val.v + case <-notify: // new task was added, loop again to ensure we have the earliest task. + if !timer.Stop() { + <-timer.C + } + continue + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + var zero T + return zero + } + } +} + +type timeQueueEntry[T any] struct { + at time.Time + v T +} + +func (t timeQueueEntry[T]) Less(other timeQueueEntry[T]) bool { + return t.at.Before(other.at) +} + +func (t timeQueueEntry[T]) Eq(other timeQueueEntry[T]) bool { + return t.at.Equal(other.at) +} + +type equals[T any] interface { + Eq(other T) bool +} diff --git a/internal/queue/timequeue_test.go b/internal/queue/timequeue_test.go new file mode 100644 index 000000000..7dc623127 --- /dev/null +++ b/internal/queue/timequeue_test.go @@ -0,0 +1,87 @@ +package queue + +import ( + "context" + "math/rand" + "slices" + "testing" + "time" +) + +func TestTimeQueue(t *testing.T) { + t.Parallel() + tqueue := NewTimeQueue[val]() + + for i := 0; i < 100; i++ { + tqueue.Enqueue(time.Now().Add(time.Millisecond*time.Duration(i*10)), val{v: i}) + } + + for i := 0; i < 100; i++ { + v := tqueue.Dequeue(context.Background()) + if v.v != i { + t.Errorf("expected %d, got %d", i, v.v) + } + } +} + +func TestFuzzTimeQueue(t *testing.T) { + t.Parallel() + + // generate random values and enqueue them + values := make([]val, 100) + for i := 0; i < 100; i++ { + values[i] = val{v: rand.Intn(1000) - 500} + } + + tqueue := NewTimeQueue[val]() + now := time.Now() + for _, v := range values { + tqueue.Enqueue(now.Add(time.Millisecond*time.Duration(v.v)), v) + } + + slices.SortFunc(values, func(i, j val) int { + if i.v > j.v { + return 1 + } + return -1 + }) + + // dequeue the values and check if they are in the correct order + for i := 0; i < 100; i++ { + v := tqueue.Dequeue(context.Background()) + if v.v != values[i].v { + t.Errorf("expected %d, got %d", values[i].v, v.v) + } + } +} + +func TestTimeQueueEnqueueWhileWaiting(t *testing.T) { + t.Parallel() + + tqueue := NewTimeQueue[val]() + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) + defer cancel() + + go func() { + time.Sleep(time.Millisecond * 50) + tqueue.Enqueue(time.Now(), val{v: 1}) + }() + + v := tqueue.Dequeue(ctx) + if v.v != 1 { + t.Errorf("expected 1, got %d", v.v) + } +} + +func TestTimeQueueDequeueTimeout(t *testing.T) { + t.Parallel() + + tqueue := NewTimeQueue[val]() + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) + defer cancel() + + v := tqueue.Dequeue(ctx) + if v.v != 0 { + t.Errorf("expected 0, got %d", v.v) + } +} diff --git a/internal/resticinstaller/downloadhelper.go b/internal/resticinstaller/downloadhelper.go new file mode 100644 index 000000000..f35d76be0 --- /dev/null +++ b/internal/resticinstaller/downloadhelper.go @@ -0,0 +1,101 @@ +package resticinstaller + +import ( + "archive/zip" + "bytes" + "compress/bzip2" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "strings" + + "go.uber.org/zap" +) + +// getURL downloads the given url and returns the response body as a string. +func getURL(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("http GET %v: %w", url, err) + } + defer resp.Body.Close() + + var body bytes.Buffer + _, err = io.Copy(&body, resp.Body) + if err != nil { + return nil, fmt.Errorf("copy response body to buffer: %w", err) + } + return body.Bytes(), nil +} + +// downloadFile downloads a file from the given url and saves it to the given path. The sha256 checksum of the file is returned on success. +func downloadFile(url string, downloadPath string) (string, error) { + // Download ur as a file and save it to path + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/octet-stream") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("http GET %v: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http GET %v: %v", url, resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response body: %w", err) + } + hash := sha256.Sum256(body) + + if strings.HasSuffix(url, ".bz2") { + zap.S().Infof("decompressing bz2 archive (size=%v)...", len(body)) + body, err = io.ReadAll(bzip2.NewReader(bytes.NewReader(body))) + if err != nil { + return "", fmt.Errorf("bz2 decompress body: %w", err) + } + } else if strings.HasSuffix(url, ".zip") { + zap.S().Infof("decompressing zip archive (size=%v)...", len(body)) + + archive, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return "", fmt.Errorf("open zip archive: %w", err) + } + + if len(archive.File) != 1 { + return "", fmt.Errorf("expected zip archive to contain exactly one file, got %v", len(archive.File)) + } + f, err := archive.File[0].Open() + if err != nil { + return "", fmt.Errorf("open zip archive file %v: %w", archive.File[0].Name, err) + } + + body, err = io.ReadAll(f) + if err != nil { + return "", fmt.Errorf("read zip archive file %v: %w", archive.File[0].Name, err) + } + } + + out, err := os.Create(downloadPath) + if err != nil { + return "", fmt.Errorf("create file %v: %w", downloadPath, err) + } + defer out.Close() + if err != nil { + return "", fmt.Errorf("create file %v: %w", downloadPath, err) + } + _, err = io.Copy(out, bytes.NewReader(body)) + if err != nil { + return "", fmt.Errorf("copy response body to file %v: %w", downloadPath, err) + } + + return hex.EncodeToString(hash[:]), nil +} diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go new file mode 100644 index 000000000..56607aed2 --- /dev/null +++ b/internal/resticinstaller/resticinstaller.go @@ -0,0 +1,235 @@ +package resticinstaller + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + + "github.com/garethgeorge/backrest/internal/env" + "github.com/gofrs/flock" + "go.uber.org/zap" +) + +var ( + ErrResticNotFound = errors.New("no restic binary") +) + +var ( + RequiredResticVersion = "0.18.0" + + tryFindRestic sync.Once + findResticErr error + foundResticPath string +) + +func getResticVersion(binary string) (string, error) { + cmd := exec.Command(binary, "version") + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("exec %v: %w", cmd.String(), err) + } + match := regexp.MustCompile(`restic\s+((\d+\.\d+\.\d+))`).FindSubmatch(out) + if len(match) < 2 { + return "", fmt.Errorf("could not find restic version in output: %s", out) + } + return string(match[1]), nil +} + +func assertResticVersion(binary string) error { + if version, err := getResticVersion(binary); err != nil { + return fmt.Errorf("determine restic version: %w", err) + } else if version != RequiredResticVersion { + return fmt.Errorf("want restic %v but found version %v", RequiredResticVersion, version) + } + return nil +} + +func resticDownloadURL(version string) string { + if runtime.GOOS == "windows" { + // restic is only built for 386 and amd64 on Windows, default to amd64 for other platforms (e.g. arm64.) + arch := "amd64" + if runtime.GOARCH == "386" || runtime.GOARCH == "amd64" { + arch = runtime.GOARCH + } + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/restic_%v_windows_%v.zip", version, version, arch) + } + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/restic_%v_%v_%v.bz2", version, version, runtime.GOOS, runtime.GOARCH) +} + +func hashDownloadURL(version string) string { + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/SHA256SUMS", version) +} + +func sigDownloadURL(version string) string { + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/SHA256SUMS.asc", version) +} + +func verify(sha256 string) error { + sha256sums, err := getURL(hashDownloadURL(RequiredResticVersion)) + if err != nil { + return fmt.Errorf("get sha256sums: %w", err) + } + + signature, err := getURL(sigDownloadURL(RequiredResticVersion)) + if err != nil { + return fmt.Errorf("get signature: %w", err) + } + + if ok, err := gpgVerify(sha256sums, signature); !ok || err != nil { + return fmt.Errorf("gpg verification failed: ok=%v err=%v", ok, err) + } + + if !strings.Contains(string(sha256sums), sha256) { + fmt.Fprintf(os.Stderr, "sha256sums:\n%v\n", string(sha256sums)) + return fmt.Errorf("sha256sums do not contain %v", sha256) + } + + return nil +} + +func installResticIfNotExists(resticInstallPath string) error { + if _, err := os.Stat(resticInstallPath); err == nil { + // file is now installed, probably by another process. We can return. + return nil + } + + if err := os.MkdirAll(path.Dir(resticInstallPath), 0755); err != nil { + return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } + + hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") + if err != nil { + return err + } + + if err := verify(hash); err != nil { + os.Remove(resticInstallPath) // try to remove the bad binary. + return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) + } + + if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { + return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) + } + + if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { + return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) + } + + return nil +} + +func removeOldVersions(installDir string) { + files, err := os.ReadDir(installDir) + if err != nil { + zap.S().Errorf("remove old restic versions: read dir %v: %v", installDir, err) + return + } + + for _, file := range files { + if !strings.HasPrefix(file.Name(), "restic-") || strings.Contains(file.Name(), RequiredResticVersion) { + continue + } + + if err := os.Remove(path.Join(installDir, file.Name())); err != nil { + zap.S().Errorf("remove old restic version %v: %v", file.Name(), err) + } + } +} + +func installResticHelper(resticInstallPath string) { + if _, err := os.Stat(resticInstallPath); err == nil { + zap.S().Infof("replacing restic binary in data dir due to failed check: %w", err) + if err := os.Remove(resticInstallPath); err != nil { + zap.S().Errorf("failed to remove old restic binary %v: %v", resticInstallPath, err) + } + } + + zap.S().Infof("downloading restic %v to %v...", RequiredResticVersion, resticInstallPath) + if err := installResticIfNotExists(resticInstallPath); err != nil { + zap.S().Errorf("failed to install restic %v: %v", RequiredResticVersion, err) + return + } + zap.S().Infof("installed restic %v", RequiredResticVersion) + + // TODO: this check is no longer needed, remove it after a few releases. + removeOldVersions(path.Dir(resticInstallPath)) +} + +func tryFindOrInstall() (string, error) { + // Check if restic is provided. + resticBinOverride := env.ResticBinPath() + if resticBinOverride != "" { + if err := assertResticVersion(resticBinOverride); err != nil { + zap.S().Warnf("restic binary %q may not be supported by backrest: %v", resticBinOverride, err) + } + + if _, err := os.Stat(resticBinOverride); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("check if restic binary exists at %v: %v", resticBinOverride, err) + } + return "", fmt.Errorf("no restic binary found at %v", resticBinOverride) + } + return resticBinOverride, nil + } + + // Search the PATH for the specific restic version. + if binPath, err := exec.LookPath("restic"); err == nil { + if err := assertResticVersion(binPath); err == nil { + zap.S().Infof("restic binary %q in $PATH matches required version %v, it will be used for backrest commands", binPath, RequiredResticVersion) + return binPath, nil + } else { + zap.S().Infof("restic binary %q in $PATH is not being used, it may not be supported by backrest: %v", binPath, err) + } + } + + // Check for restic installation in data directory. + var resticInstallPath string + if runtime.GOOS == "windows" { + // on windows use a path relative to the executable. + resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), "restic.exe")) + } else { + resticInstallPath = filepath.Join(env.DataDir(), "restic") + } + if err := os.MkdirAll(filepath.Dir(resticInstallPath), 0700); err != nil { + return "", fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } + + // Install restic if not found OR if the version is not the required version + if err := assertResticVersion(resticInstallPath); err != nil { + lock := flock.New(filepath.Join(filepath.Dir(resticInstallPath), "install.lock")) + if err := lock.Lock(); err != nil { + return "", fmt.Errorf("acquire lock on restic install dir %v: %v", lock.Path(), err) + } + defer lock.Unlock() + + // Check again after acquiring the lock. + if err := assertResticVersion(resticInstallPath); err != nil { + installResticHelper(resticInstallPath) + } + } + + zap.S().Infof("restic binary %v in data dir will be used as no system install matching required version %v is found", resticInstallPath, RequiredResticVersion) + return resticInstallPath, nil +} + +// FindOrInstallResticBinary first tries to find the restic binary if provided as an environment variable. Otherwise it downloads restic if not already installed. +func FindOrInstallResticBinary() (string, error) { + tryFindRestic.Do(func() { + foundResticPath, findResticErr = tryFindOrInstall() + }) + + if findResticErr != nil { + return "", findResticErr + } + if foundResticPath == "" { + return "", ErrResticNotFound + } + return foundResticPath, nil +} diff --git a/internal/resticinstaller/verify.go b/internal/resticinstaller/verify.go new file mode 100644 index 000000000..49610a448 --- /dev/null +++ b/internal/resticinstaller/verify.go @@ -0,0 +1,188 @@ +package resticinstaller + +// Original implementation in restic: https://github.com/restic/restic/blob/master/internal/selfupdate/verify.go + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/openpgp" +) + +var key = []byte(` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFRVIb8BEADUex/4rH/aeR3CN044zqFD45SKUh/8pC44Bw85iRSSE9xEZsLB +LUF6ZtT3HNXfxh7TRpTeHnXABnr8EtNwsmMjItDaSClf5jM0qKVfRIHBZ2N539oF +lHiCEsg+Q6kJEXHSbqder21goihfcjJBVKFX6ULgCbymOu03fzbhe/m5R57gDU2H ++gcgoI6a5ib11oq2pRdbC9NkEg7YXHbMlZ5s6fIAgklyDQqAlH8QNiRYcyC/4NrG +WXLwUTDssFn3hoJlAxZwj+dRZAit6Hgj2US05Ra/gJqZWzKyE2ywglO9sc2wD3sE +0Ti1tS9VJr7WNcZzVMXj1qBIlBkl4/E5tIiNEZ5BrAhmdSYbZvP2cb6RFn5clKh9 +i+XpeBIGiuAUgXTcV/+OBHjLq+Aeastktk7zaZ9QQoRMHksG02hPI7Z7iIRrhhgD +xsM2XAkwZXp21lpZtkEGYc2qo5ddu+qdZ1tHf5HqJ4JHj2hoRdr4nL6cwA8TlCSc +9PIifkKWVhMSEnkF2PXi+FZqkPnt1sO27Xt5i3BuaWmWig6gB0qh+7sW4o371MpZ +8SPKZgoFA5kJlqkOoSwZyY4M7TRR+GbZuZARUS+BTLsAeJ5Gik9Lhe1saE5UGncf +wYmh+sOi4vRDyoSkPthnBvvlmHp7yo7MiNAUPWHiuv2FWU0rPwB05NOinQARAQAB +tChBbGV4YW5kZXIgTmV1bWFubiA8YWxleGFuZGVyQGJ1bXBlcm4uZGU+iQI6BBMB +CAAkAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheABQJUVSmNAhkBAAoJEJGmhovT +96kHQUcQALfi9KohoE0JFkKfSXl5jbBJkTt38srMnZ6xKP45F0e/ir1duFVCSyhZ ++YS/n6aBMQl/qRWbzF+93RnGsTLvMi/8Oa72czlEPuYYfFPuJAatxvA/TFZHuI++ +u6xAF4Oxlq0FAbEJfpw0uLSDuU9f9TlLYNP3hLudjFFd9sJGLLs+SCeomPRKFxRR +LL7/1EzdtmvvFhZZcuPsTamBb4oi+1usCO5RW1AQA5A4Qo4gHitBaSaBgolFZLN7 +6UFBwBs/t0hDZPAAZa1T8EpjQrlmINFIeBYFdvjhMChGQc6NcfOOQofW5BDVn6Gs +BHYTvAgSK5G0eaB+bOAtv9LW9hDt05iEJaE5ojPT7ThicHoU65WL4yGAGCGcfNm+ +EpuNGt1IgAFGGxX6wMZy59WqtMBZANjWQdrDbCPQa3pIID96iN0A1HZJg7eAl0y3 +NM6nU7faAuW4QOoQRgxOTj0+fM6khaFmYCp5nuer3d5pkaH6SQG4ZDVOOSaak7Ql +T/EFSfz2B5FZN1OIdqw5/Aw7HugOehHund5QfgRuDLSqZKnuGrIo9OwJIirT/TDD +nsNWBTN3Pxf1h8Iut+R9Zt7LwsVjVN9+JPL8yEk4zzCxHEL4/2c6jANQdtbQCZiH +bU85JWe1NKFo/NNPpM2ysZMpKHe5RB3FLMare0IBs5BO06nyGpTmiEYEEBEIAAYF +AlRVKToACgkQFBE43aPkXWafmgCfcR+LfsAn6aZxjX46Km5tmWpDVrAAoJfHpfBG +5MEki2MOoKvEsWDZqhHSiQIcBBABCAAGBQJUZRF9AAoJEGo6ELMcEJUXrtIP/iJh +0i7VaQG2F+QqGW8Evqng33HeLzW9BvYxt3X1QNb4ylFIc51Hl33yh5ZkPmZk/I3m +BaDd23IE2rhzETxDGrAMnE6zeaQ+iTu6iySBxqHjtK0HwKObuBA4Sw803Hn31Owa +Z8a3TEUkyiHPh8NBuxbvXNuOrxsglATE4KCuUGjGdmNs1raG9mqSUXgZCh1q1kAI +NN6O9DFFS1RsAvNK0qmTZZMfHWZeu10O55MHsxTsfPY/v1Jphg+vHc2NItw0s23R +j6SJN5fgNSLhcKBdCRpw33YFy+EWA8lE2FRd5DStn2sNWvAOoWLrIHZo0UgrgFV2 +gi4QpaN+b/T+QDiq7IcwLaMSWU3ODYIFN2C/TBKZRIC7LWQPG0cjFJd/kWDQWB+i +/MdYMOOuDo6ohh3vfkC7xNEo3lJArC3Zgf4SmO6OBMnIdYjdchDV8dn5lSbKq1A3 +20FIUWIxdkfx4L88J3KOGMAmuZxmnWkKN6iEg+Pb/axuX0cHSx/j7vV01YY2Z6j6 +98tKhP2XObH990Eqfr8zJcj0tCuKEHww7Pn8aH9BHvig5KeEAIbggW34jR9TUKXd +1ao5HX0pKSa/37OqlG4r+XUORCV7awNSuUHU8BR0emDCsgRdrQ4szRgJLOA8sP14 +b9fz+xO3KKNrDXGYZLXFSVwGzrSC5zbh6QkC0qeYiQIcBBABCgAGBQJUamlAAAoJ +EFKZtJkEN6xByy4QAMQJ45eOJtVyGtCKtIr2mPZQ0VI60hfMB6PVFlb7DOpWIHoh +Nl6KWzZunENelXp+VNQcj6St5iZrdOiyiMOY/FdN0rhPAYWERchABd9WDS9ycBr8 +n5kWmB36Wa2r/aTOlDYJ/botigS6To6bR6Gc9FEj4QuVnmqzMlawSz/O0HNS0Hej +DgUwgR3hCDAAp/Hw9PR1CRcHw2bo/B5+GEcl+w6iAkheGXuV2zSWXf6LRLRSEQ70 +f6n4hs6vsuQQ35yd4UXy/t/q3l7/xeJ5TBWWiXviQK1tIOsUJ+/cCpWzms+IFvt+ +UsTQBMMuKBFqjkl4oDgtv8vf1i2NZsNo/XbzPB4hua5HyBuhn0/ej9zMfmMvfqZG +6ZzaAGpZYRCVRcw3nc0yNnoW+g7pAJs1M3sL1BXpUGfROG/T3yFzL+sk62moG2tD +G2G7QsNVzOxRDi8bax5f3U7QW1i33o3qRbr9BfePyWtfVuWHisTw1rBdwwEAfYQs +YUSDMXcTB9LhUWAhJAtEeLz0GOaA+IlafVwqmIdLxTsUoNYfpgRi2Wic33oRI1UG +yENtzKUu1iaHvSCEd/obvrhpx353oI4Yq8Gkr8mWRptX9IGXa+qASZ9gMxJ1PoPA +dLG5/oECVC/ORaNAL3zY9SbmGWamcWgSAeIB3iJxQlyMYikLDzb390y+5AFXiQI3 +BBMBCAAhBQJUVSG/AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEJGmhovT +96kH8pAP/3PtPkxkYNaS+PXiHBDpDnUsBiwvZt3FsebexepAepzL8dP00oNyI7dP +F2XKm4e/PHJ0nnY+tD4KKRdBeiQOL5ZywHmxZX04P1/Z6uCbVpGCSovcWavBkP8A +k+/CjzJUA6Z/s17D6LIpDDntn6v0abRoTy3asexG277udP+iO+1q/mnxSiSZzNas +rh973gXSeqL3oV+oY6DCSPpOSJlbI85UMU5/WnCxPIVHvaDG8Fv5RF74d3+FVJKd +7TRnUsqZ4MLI++JNXwK0O8dCQ8NsB3NF2rDnND+zhzDlisvdiyGsQUNMnn1Czi4D +/MK2/2xkdoVzKNA0v3IHlnxMWhZVLaHqUiGYUGF6NsB+OgXEEJRGIpukYnd/+WkL +Qqfzyy6Y6uUDhkwz0G5aDGyNUg7+gfDMr5dy+HxtgEzkcoZJWuNBzO0Yp198QNNE ++QBxu9OkYw9A8wT58cHVuzFU0V+bTBrZtpbME8OWLy6+eDXn6CbVEu2Fc92ckQoz +EpRdZMdiWVtbQDY8L9qAiC+BOVqBgv5PoB8IVHrV1GmRZwxRdlplnzviWa5Qvke5 +dcUy+DXmrCN+dWUql8fFt2g6EIncFYotwxz7r3+KdjCFKzG6zmMLHBCUz0exAxrH +4vXXB1LdEByddgjXcol+N73D4jUyYvs12ziecGitAU8z9nYK347XiQIcBBABAgAG +BQJWBrfPAAoJEB5F+Mqd4jsWKCoQAIgC4I+rQ1UivkF1sk0A6IUvWAP4yfNBM6UP +K5bxFLd/kjL9LiiWD74NVmQ8WAa1Y3cv/gVrmekcVhxXGf3bZcGKBnYmmpR0xWli +XxQJ9CGPwtoaN7ZDKPbOKFzOHX3DiCZ9ePrxwpgym5iu541X2aGH/cqh9bPS4vxv +Nez+5fwhSdDe6iJZ09/oiJuMkyir3SKx3SJRf3Yd2G2k2bPJae2NjiolKIJrgNMe +SSYahaMHF8i+UpUQMqXK4vAWPFML9DzwjJVbnJuJ8s9T/5wy3Lw33Fpe33a8bTon +bEk60+NwhlnRAho0J5At9LVpTUuA0+5LAT2kwAzk2dPzYJl9SVYOQWP8Zj4tEDGF +hQPfdnYMhQB2ECTrtLBLJbqnEbCgRA7VTlGnsb+PU+Ut1GLhglPFPRjfAhWRKBLe +9sDYn8icrhJqvEyc8YMjeBSMEuQUm65b0fjUUl9eBSdRxy2RkQiPTg+o8kLOOnt6 ++ar3S+JXIcN4GpLfBt5cpBiU53TkuTJYHqIHqKyLgEfIIfNRrKTbK6sCfA5STKTf +JSmewY2vGM7D4njQ2Iz8a4SU/XFOWQP0zlehDe1jhLXqYBlYMyXoULkXLkMfmIZX +AHoVn7z1POa94NcKePpW2BFm4Q0OjrwY2/ufPF/RlB4qNiFsrVuWpL7eMzaMZ+JV +oZXxEPMRtChBbGV4YW5kZXIgTmV1bWFubiA8YWxleGFuZGVyQGRlYmlhbi5vcmc+ +iQIfBDABCAAJBQJW9SIDAh0gAAoJEJGmhovT96kHrP0P/24pnzm7zUyMFjUuZbsc +JxNk31K/gSWQ6S5AMPeKB/ar5OMRMkmpZZmOX8c1Q1MxdGdRGPFzA++uWPiizc3Q +LQIrzI1Q2oarkjcb3FMOMpn4M5xZp/+dmuWSrgEEF3iPom/DjpE+U/DC6/YaeJJO +WLuiU799c8b9Qg+ZZcf5L1vUMT489kDL8FgwiThoAXQ4LgSylblguVNkSiyZAQ7g +0snYD93jdBvY2KSIQ1Y9mIZPZYcZacj+CVMMAQOAP6WmrOw6hREaYFo/0Z9tMC0Q +Fba2hwAISS/hrBPFCFalq9E0tqClryitXdJp0/k8QgU979pANJXmZCvmFhjcCIKg +9ok7+lykFmbo+UCmRRoYoLlaw4wNfuN3TIlDyWx7cfAVww+AwQD8E1k6jXJpqT5s +Y+NSbJ2bPRR+AQk3qkvU2dJqOIJxF02jp4a4QxypTAN+byCkJcnrl7XMcykAeCAf +XIA5xRoZu44WJhHmTIAMf5SLzk889MggQrGVKckOpvSaFDElqW54DY/erkwFiZKd +t0rOmvqY4/63Btw6x7Y63THp4xf5IvFf0REc/Eh5aC0gPilHPS9ZbuIh0tX4hrQY +J2SPQ5bU63XC+ucJrHde25dDEa9oQ/xny3Dd233j8ofdLuBKejXXjhD/Dv3nlAEZ +D9VQgaF4kQcpqkz+dsgzEA3IiQI3BBMBCAAhBQJUVSlqAhsDBQsJCAcDBRUKCQgL +BRYCAwEAAh4BAheAAAoJEJGmhovT96kH/JMQAIQLk13LPm2K7YvLrO0iq8MbM/YK +pUh97sms5ItVMZJm3tGmbc4bgOJ2zAfeRRoumMqIyv2KLuXNKdysoGIowKvukOEK +v3IFv1pIXYwQ7KrRa+Pn1kfpjgoOePN/gm5fbGZTgRe65a9XhkBPKB4emv3hrX2b +WFMxtkbzDyP03oshTO/tpBFMNV+XA/Zlz3fLzvICUzD1SHTzzTFACyFkiB68Yx4y +UXXAln7LCXzHsdiM/3EuloiDZtao5t1Ax0+GEbo7WL3bIoR8e1q4d/PgbKPQvASH +jw/s0S1gCwOnFeoShrn2ijp2/5XLVjx4hQ/CCv+RJxv1lNzmmZtBBMGHbl1U5rcE +Ys/Fpe1If1RyC83mimmMfGS5TTXOMWbqjNlRT9bLRM/+OLvS6FWuAuFogQkCc+pa +VKHSEQJiN2+2XbjfrrozubB6Icegp2RwKyi9BRre9V6NPfEm1C8d6hmloqsK5RHX +z62a25sH4mEufTxgYn7TCxx+wckBWOlDe3p2i1lJDv1SKQXN0ZANfmObdfUMYyhZ +/j/ariRx1uhSrgPzQoRBaDMa/klqGyWQ9Yh1nJdeKwPZo4zriZvK5LAgnWh2IRcG +iPtSdtdk5KaEAouUR8XNGEpL79+Awi3kjd5uEmnu+ZqDDM/hq3NgzbQ51PuwBeuH +pe+6S4onnc5Sh5yoiEYEEBEIAAYFAlRVKXwACgkQFBE43aPkXWYUtQCfW61UqGPh +e0atXSnkzEevKm6y99QAn1CZ4rCVg4u/Zp6nvKncdd3cs0/NiQIcBBABCAAGBQJU +ZRF9AAoJEGo6ELMcEJUXk0gP/RJg5pLpPNibz09kjwsOJAlpORyd6OBxE4tOn/bE +CT0mE6vBg0TdY+MO1IC0o5RkiAc9f5YizzYyLBVpfgwdrfCk+eD9mFKhn7szYI23 +2AdXIO5ziC4pND+zdkSj37fxAcM4BIfeyHWKna/cmkM1tsmB1YYpxNpM56Y+ulAG +6YJBkU0hPoUjI7eNMsV/+V/IOSuP4Z/Lhw1fw4bKow0zVc20C+dbgrBz9uKGUrmT +jLMLoEiaxn7yrYug8kQeeKaEoczyQhivQrKFZsfRMkiGaRz7qeOWrw10MCPNa3LX +wswXpxM9FGLnflOwhUiYSgD1OdmBaEEntDPX75Dp0n8pdm5GNFCuD9RpJrGIiPm5 +dsU8kRMeFbUQFNOkJE5Npxv7DrmTIjFd90U3kwcwEL0Y++W+q/lbrmxgOuYmc/c6 +K8WVGjsOTpEuqFvmLmhIwxzH4QCtSUTb/O2bg7PIbAk2LVMbXi4H9Fxl8YCWb532 +Tu2RQ73Odvmluhj+QTFnxglEd4xiOlttwIOwqQAgLBk/GSioFfgLaGla5iabGaWu +MB6zFiDp30IDHfIchUp/jaBvWJf33UaemryRrppVv1mgs6qvKxbGmYSOn7I8KBas +iyV2IXaUCHDdreFcCLJrl/Cso9qQcHroI127IQAB5upyN3TuS0RS/ZnAZc9yd0Jx +kFNHiQIcBBABCgAGBQJUammYAAoJEFKZtJkEN6xBoC8P/2ipNFdW6rMuISzGUcGs +CQVNcil/9mZ+iOqe+7DS356vJmENvof31r2/tTHUcJcRoh7ANkR0YuvZylD8MFXk +jrAj+X2ODSCsaugyjxWEg5XEYLnHipX7eFxzT39UJrgP/4wNu8tWDO6t/xhblHUi +chE1tvWZkUnWzhQrBKIiYGZnu0mxIEHR33PZauc4vFL2U0K8deKpo01jtbz9f8+n +grcTplCfCJ8H0SoR/8t4qyg2FNgcnJW7F/VVa3j6ctDBkB+NcPYjeRL8cybHV0VU +xbbG64D+WbqsspWDRj7799SELQ5emUnyok0j/e+3ffFkiKP0mpMX3RW2MfMxd1Bs +AoY3IqvIdlAjrtAY2tQ3sXAyPgmcp6kKRKixLTbBjGfrptNLO2nzADvHk3/4OnKl +tlYj406A7ZgeKDWku+yQ8VSPCeFh86KwiQBgoZOJiQSRWYGT2hy8xLb4im4W1cor +bfL+2+iVMu+EwQ6QFlyzaLgNPNWBXUWvK8vok55LNuHYKHxCJD3b53vBctmB08/U +piRMDT5JTolyfwc4zbFrgb4d5lvTP0bM2qIHoEde5GyDKaUZkHvbBKokkR7nMKhK +mfWs091mG5AP03NXdmt/mllv3bRPsbJJTP0m8BMliQjvPIhKk7ngbNHefzGCdv06 +psDi8Q8apCdR2bLDaLp2HC4TiQIcBBABAgAGBQJWBrfPAAoJEB5F+Mqd4jsWMmYP +/0izNHAqY8HvpyM7SlWixvXI2tjWSlhiC8dLv77rDLTjW/XbKh8P+6abxaPBg9dF +xHGDBJli0U9J+Mp5AodB+b/xgA8ro/U5sGGvTVI02AE9ohPwR2W6xePOapmkyWxO +P4kfEP8bK2V/JnBdk8Rq6tce5onBWTrFQCcqs2OprlfPpbKZgQ6b/K7nNP9uv2ku +twhSxmw4NpKJmvGzf1HnWVQKaE4yqCoH9pJGyQmn+v8JBytkRIsAhlV/HKC8Toz5 +x933qAHVMW/Xf7hYWu9a15rS8NSBMOEFmvKubxYbqEPYM5uXENDCa+oGSbx52crf +IoOfiWWSiDyV502kZ4WB0jOdwITCDp6D/Hq4HZMnoquZ9Kk7mRuOP3FGXtAmp3rY +9TGBcjH2hjA1xen0N59uaE+zNeU0wEhrSMbwUvsFt6Z/p8aS8TMZIayVbmGi/0Vf +UonjIx3OPsd08CfXTk9cY+R3RKoFaJBBa2ZK2XnXj7VgbdcYx7IV4G41cxOHCgCl +BAMyzKI+UHuN4H39sQixFJW7tF0QJrYaXamuzIyh99qy5b0NvWxCiStZOKglmq2P +61ryqzyPcbwEn+1OeXUXayZslU8M2SMRfA3qXaBUuH5fQgVzCm8DndwdGRbh0ceo +E4BtZkZ3hmWvpPgt177eLcg7plL176bnjV0llspg+ji5uQINBFRVIb8BEADo4td9 +MrPJd0wt2Q0OPgdAOyxpwGgu2vh8TTL5sUsMpJEKRQdc5AyEI1/mrTINDVgTSjTd +VPQE8fb4w3GHAUg4iBPucyGLUpQd+pxYya/aqVurKjynVZPHpZzCylsdVv8WR1Bb +bVIbmPiJxmRi3irjNzsmCeUV1V8JPpMxWBdV14NTcRkeJA2JpRXp8ZHhO9WryZV9 +uxxMiDS8NIlAI6Ljt1swrJQOv2sHk9Gbrgmpd1zTYjJzORXZHsQdQ6XAy/4yWwt8 +Gl+eg5ZRSyAE80TEIH0FFJcQ/9YZK/j9bxN+wGiuW4goNdBl84NJ8aq1G0NXDjyH +9WWypWfgURUoNBVmSek2ibRxSriqdFH8Tt+98w1a8EdLJKbPb0A5sV6PqqKUP59a +1AZ1kA0tLjh89Wz6+qjg9YhiCN7SO6eikdPWT/0r3SHtiztgDjgcqTFDNoFZdmZc +jb6eD0nuoRRfWXVZ57aX8WwD37xljKt7e06W7gsq4fXyRYZvQpNHga+83YCkVbxu +pPgPjgq4F/JquIUVfOx3CMmLsvE5p2U0zLGzG1WYgW5AShDfo2LXtjOz4wmRFnfY +pFO+CreWiG3OElwae77JiHXSc7+8pCOE3Buh9SRI8ioJPhb4uxV3paFH9uDTQjpC +nVMI5uOHg0tmWZgTShB/tzDV1KFVTZCw3fABxwARAQABiQIfBBgBCAAJBQJUVSG/ +AhsMAAoJEJGmhovT96kHb/0P/0LXAOXeyTDWOHEoPFKIXS4y9tl2PA6hq1HOgthP +1B2P6eIjpB7UGTSieWpKeqkkv7SZFTol2H0JlhZlhh1IkxS/aHHAl2Km6TLkk6QL +GGkKOFFAiU51iVkJQumbTKMlx11DXA0Jy6mVsUWoz3Ua9cFwrhuCRpKxW61xTEaX +dksgOUBKWH+mF8MtJtRedwHXjmNxaKTAKEsjmPFPn8i75D48JIbq9L+rHLxFTeSR +LShj7lZR1I24+UofA2Tllh4V14rSsUkfIYsKuwCGenJ+sPhpwqHohfJzTewXk+TK +wkilwVgTg7AYCeywP7XqkhA4om9aJRc1cqPcrknsXJLz4Vp7JX8bCtRqF2JT7wsM +wtHMNAtItLa+WYnkvt9/ng9Zt5i0fHZBwfVazWP+/4LAkb9fE4vO2IusV0jK00Sk +7Gt65A32qY75Lze6NRUk2gwizMLIdMvag9AuIUH52RScNVoVXIkmw1q57KshBL1M +VWRd7DUpFGpw8HKkqNlJKPAv+UsJAp7rSkfH9CAYwFzjbs7BST5Cuynac0CgZGQO +F0793mKAsbMePuEIzkR0ZdA/F0Mar9/tQLAtU3pXRrThkLUNmr8Qm9rPGTjrNv7k +ANWsgd4bu0PW5SVm+eFjoTRpNI9P/xrCF8fgLcZ2JPO/wKqyIDcKxEZq978lxWDm +CwGc +=AV20 +-----END PGP PUBLIC KEY BLOCK----- +`) + +// gpgVerify checks the authenticity of data by verifying the signature sig, +// which must be ASCII armored (base64). When the signature matches, GPGVerify +// returns true and a nil error. +func gpgVerify(data, sig []byte) (ok bool, err error) { + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) + if err != nil { + return false, fmt.Errorf("read keyring fialed: %w", err) + } + + _, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(sig)) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/internal/test/helpers/testdata.go b/internal/test/helpers/testdata.go deleted file mode 100644 index dfc79d0a0..000000000 --- a/internal/test/helpers/testdata.go +++ /dev/null @@ -1,21 +0,0 @@ -package test - -import ( - "fmt" - "os" - "path" - "testing" -) - -func CreateTestData(t *testing.T) string { - t.Helper() - dir := t.TempDir() - - for i := 0; i < 100; i++ { - err := os.WriteFile(path.Join(dir, fmt.Sprintf("file%2d", i)), []byte(fmt.Sprintf("test data %d", i)), 0644) - if err != nil { - t.Fatalf("failed to create test data: %v", err) - } - } - return dir -} \ No newline at end of file diff --git a/internal/testutil/deadline.go b/internal/testutil/deadline.go new file mode 100644 index 000000000..120d3d05e --- /dev/null +++ b/internal/testutil/deadline.go @@ -0,0 +1,16 @@ +package testutil + +import ( + "context" + "testing" + "time" +) + +var defaultDeadlineMargin = 5 * time.Second + +func WithDeadlineFromTest(t *testing.T, ctx context.Context) (context.Context, context.CancelFunc) { + if deadline, ok := t.Deadline(); ok { + return context.WithDeadline(ctx, deadline.Add(-defaultDeadlineMargin)) + } + return ctx, func() {} +} diff --git a/internal/testutil/logging.go b/internal/testutil/logging.go new file mode 100644 index 000000000..f6f2a9222 --- /dev/null +++ b/internal/testutil/logging.go @@ -0,0 +1,28 @@ +package testutil + +import ( + "strings" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type testLogger struct { + t *testing.T +} + +func (l *testLogger) Write(p []byte) (n int, err error) { + l.t.Log("global log: " + strings.Trim(string(p), "\n")) + return len(p), nil +} + +func InstallZapLogger(t *testing.T) { + t.Helper() + logger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), + zapcore.AddSync(&testLogger{t: t}), + zapcore.DebugLevel, + )) + zap.ReplaceGlobals(logger) +} diff --git a/internal/testutil/operations.go b/internal/testutil/operations.go new file mode 100644 index 000000000..126636bce --- /dev/null +++ b/internal/testutil/operations.go @@ -0,0 +1,64 @@ +package testutil + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "sync/atomic" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/cryptoutil" + "google.golang.org/protobuf/proto" +) + +var nextRandomOperationTimeMillis atomic.Int64 + +func OperationsWithDefaults(op *v1.Operation, ops []*v1.Operation) []*v1.Operation { + var newOps []*v1.Operation + for _, o := range ops { + copy := proto.Clone(o).(*v1.Operation) + proto.Merge(copy, op) + newOps = append(newOps, copy) + } + + return newOps +} + +func RandomOperation() *v1.Operation { + randomPlanID := "plan" + randomString(5) + randomRepoID := "repo" + randomString(5) + randomRepoGUID := cryptoutil.MustRandomID(cryptoutil.DefaultIDBits) + randomInstanceID := "instance" + randomString(5) + + return &v1.Operation{ + UnixTimeStartMs: nextRandomOperationTimeMillis.Add(1000), + PlanId: randomPlanID, + RepoId: randomRepoID, + RepoGuid: randomRepoGUID, + InstanceId: randomInstanceID, + Op: &v1.Operation_OperationBackup{}, + FlowId: randomInt(), + OriginalId: randomInt(), + OriginalFlowId: randomInt(), + Modno: randomInt(), + Status: v1.OperationStatus_STATUS_INPROGRESS, + } +} + +func randomString(length int) string { + randomBytes := make([]byte, length) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + return base64.URLEncoding.EncodeToString(randomBytes) +} + +func randomInt() int64 { + randBytes := make([]byte, 8) + _, err := rand.Read(randBytes) + if err != nil { + panic(err) + } + return int64(binary.LittleEndian.Uint64(randBytes) & 0x7FFFFFFFFFFFFFFF) +} diff --git a/internal/testutil/try.go b/internal/testutil/try.go new file mode 100644 index 000000000..252935e56 --- /dev/null +++ b/internal/testutil/try.go @@ -0,0 +1,50 @@ +package testutil + +import ( + "context" + "testing" + "time" +) + +func tryHelper(t *testing.T, ctx context.Context, f func() error) error { + ctx, cancel := WithDeadlineFromTest(t, ctx) + defer cancel() + + var err error + interval := 10 * time.Millisecond + for { + timer := time.NewTimer(interval) + interval += 10 * time.Millisecond + select { + case <-ctx.Done(): + timer.Stop() + return err + case <-timer.C: + timer.Stop() + err = f() + if err == nil { + return nil + } + } + } +} + +// try is a helper that spins until the condition becomes true OR the context is done. +func Try(t *testing.T, ctx context.Context, f func() error) { + t.Helper() + if err := tryHelper(t, ctx, f); err != nil { + t.Fatalf("timeout before OK: %v", err) + } +} + +func TryNonfatal(t *testing.T, ctx context.Context, f func() error) { + t.Helper() + if err := tryHelper(t, ctx, f); err != nil { + t.Errorf("timeout before OK: %v", err) + } +} + +func Retry(t *testing.T, ctx context.Context, f func() error) error { + t.Helper() + return tryHelper(t, ctx, f) +} diff --git a/pkg/restic/error.go b/pkg/restic/error.go index 9f458c86d..a4c9af9a0 100644 --- a/pkg/restic/error.go +++ b/pkg/restic/error.go @@ -1,21 +1,20 @@ package restic import ( + "context" "fmt" "os/exec" ) +const outputBufferLimit = 1000 + type CmdError struct { Command string - Err error - Output string + Err error } func (e *CmdError) Error() string { m := fmt.Sprintf("command %q failed: %s", e.Command, e.Err.Error()) - if e.Output != "" { - m += "\nDetails: \n" + e.Output - } return m } @@ -28,18 +27,50 @@ func (e *CmdError) Is(target error) bool { return ok } -// NewCmdError creates a new error indicating that running a command failed. -func NewCmdError(cmd *exec.Cmd, output []byte, err error) *CmdError { - cerr := &CmdError{ - Command: cmd.String(), - Err: err, +// newCmdError creates a new error indicating that running a command failed. +func newCmdError(ctx context.Context, cmd *exec.Cmd, err error) *CmdError { + shortCmd := cmd.String() + if len(shortCmd) > 100 { + shortCmd = shortCmd[:100] + "..." } - if len(output) > 0 { - if len(output) > 1000 { - output = output[:1000] - } - cerr.Output = string(output) + cerr := &CmdError{ + Command: shortCmd, + Err: err, } return cerr -} \ No newline at end of file +} + +type ErrorWithOutput struct { + Err error + Output string +} + +func (e *ErrorWithOutput) Error() string { + return fmt.Sprintf("%v\nOutput:\n%s", e.Err, e.Output) +} + +func (e *ErrorWithOutput) Unwrap() error { + return e.Err +} + +func (e *ErrorWithOutput) Is(target error) bool { + _, ok := target.(*ErrorWithOutput) + return ok +} + +// newErrorWithOutput creates a new error with the given output. +func newErrorWithOutput(err error, output string) error { + if output == "" { + return err + } + + if len(output) > outputBufferLimit { + output = output[:outputBufferLimit] + fmt.Sprintf("\n... %d bytes truncated ...\n", len(output)-outputBufferLimit) + } + + return &ErrorWithOutput{ + Err: err, + Output: output, + } +} diff --git a/pkg/restic/logging.go b/pkg/restic/logging.go new file mode 100644 index 000000000..d74f5dd37 --- /dev/null +++ b/pkg/restic/logging.go @@ -0,0 +1,17 @@ +package restic + +import ( + "context" + "io" +) + +var loggerKey = struct{}{} + +func ContextWithLogger(ctx context.Context, logger io.Writer) context.Context { + return context.WithValue(ctx, loggerKey, logger) +} + +func LoggerFromContext(ctx context.Context) io.Writer { + writer, _ := ctx.Value(loggerKey).(io.Writer) + return writer +} diff --git a/pkg/restic/outputs.go b/pkg/restic/outputs.go index 931988afb..723505dd1 100644 --- a/pkg/restic/outputs.go +++ b/pkg/restic/outputs.go @@ -2,111 +2,209 @@ package restic import ( "bufio" + "bytes" "encoding/json" + "errors" "fmt" "io" - "os/exec" - "slices" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" ) -type LsEntry struct { - Name string `json:"name"` - Type string `json:"type"` - Path string `json:"path"` - Uid int `json:"uid"` - Gid int `json:"gid"` - Size int `json:"size"` - Mode int `json:"mode"` - Mtime string `json:"mtime"` - Atime string `json:"atime"` - Ctime string `json:"ctime"` +type Snapshot struct { + Id string `json:"id"` + Time string `json:"time"` + Tree string `json:"tree"` + Paths []string `json:"paths"` + Hostname string `json:"hostname"` + Username string `json:"username"` + Tags []string `json:"tags"` + Parent string `json:"parent"` + SnapshotSummary SnapshotSummary `json:"summary"` + unixTimeMs int64 `json:"-"` } -type Snapshot struct { - Time string `json:"time"` - Tree string `json:"tree"` - Paths []string `json:"paths"` - Hostname string `json:"hostname"` - Username string `json:"username"` - Id string `json:"id"` - ShortId string `json:"short_id"` - Tags []string `json:"tags"` +func (s *Snapshot) UnixTimeMs() int64 { + if s.unixTimeMs != 0 { + return s.unixTimeMs + } + t, err := time.Parse(time.RFC3339Nano, s.Time) + if err != nil { + t = time.Unix(0, 0) + } + s.unixTimeMs = t.UnixMilli() + return s.unixTimeMs +} + +type SnapshotSummary struct { + BackupStart string `json:"backup_start"` + BackupEnd string `json:"backup_end"` + FilesNew int64 `json:"files_new"` + FilesChanged int64 `json:"files_changed"` + FilesUnmodified int64 `json:"files_unmodified"` + DirsNew int64 `json:"dirs_new"` + DirsChanged int64 `json:"dirs_changed"` + DirsUnmodified int64 `json:"dirs_unmodified"` + DataBlobs int64 `json:"data_blobs"` + TreeBlobs int64 `json:"tree_blobs"` + DataAdded int64 `json:"data_added"` + DataAddedPacked int64 `json:"data_added_packed"` + TotalFilesProcessed int64 `json:"total_files_processed"` + TotalBytesProcessed int64 `json:"total_bytes_processed"` + unixDurationMs int64 `json:"-"` +} + +// Duration returns the duration of the snapshot in milliseconds. +func (s *SnapshotSummary) DurationMs() int64 { + if s.unixDurationMs != 0 { + return s.unixDurationMs + } + start, err := time.Parse(time.RFC3339Nano, s.BackupStart) + if err != nil { + return 0 + } + end, err := time.Parse(time.RFC3339Nano, s.BackupEnd) + if err != nil { + return 0 + } + s.unixDurationMs = end.Sub(start).Milliseconds() + return s.unixDurationMs +} + +func (s *Snapshot) Validate() error { + if err := ValidateSnapshotId(s.Id); err != nil { + return fmt.Errorf("snapshot.id invalid: %v", err) + } + if s.Time == "" || s.UnixTimeMs() == 0 { + return fmt.Errorf("snapshot.time invalid: %v", s.Time) + } + return nil } type BackupProgressEntry struct { // Common fields - MessageType string `json:"message_type"` // "summary" or "status" + MessageType string `json:"message_type"` // "summary" or "status" or "error" + + // Error fields + Error any `json:"error"` + During string `json:"during"` + Item string `json:"item"` // Summary fields - FilesNew int `json:"files_new"` - FilesChanged int `json:"files_changed"` - FilesUnmodified int `json:"files_unmodified"` - DirsNew int `json:"dirs_new"` - DirsChanged int `json:"dirs_changed"` - DirsUnmodified int `json:"dirs_unmodified"` - DataBlobs int `json:"data_blobs"` - TreeBlobs int `json:"tree_blobs"` - DataAdded int `json:"data_added"` - TotalFilesProcessed int `json:"total_files_processed"` - TotalBytesProcessed int `json:"total_bytes_processed"` - TotalDuration float64 `json:"total_duration"` - SnapshotId string `json:"snapshot_id"` + FilesNew int64 `json:"files_new"` + FilesChanged int64 `json:"files_changed"` + FilesUnmodified int64 `json:"files_unmodified"` + DirsNew int64 `json:"dirs_new"` + DirsChanged int64 `json:"dirs_changed"` + DirsUnmodified int64 `json:"dirs_unmodified"` + DataBlobs int64 `json:"data_blobs"` + TreeBlobs int64 `json:"tree_blobs"` + DataAdded int64 `json:"data_added"` + TotalFilesProcessed int64 `json:"total_files_processed"` + TotalBytesProcessed int64 `json:"total_bytes_processed"` + TotalDuration float64 `json:"total_duration"` + SnapshotId string `json:"snapshot_id"` // Status fields - PercentDone float64 `json:"percent_done"` - TotalFiles int `json:"total_files"` - FilesDone int `json:"files_done"` - TotalBytes int `json:"total_bytes"` - BytesDone int `json:"bytes_done"` + PercentDone float64 `json:"percent_done"` + TotalFiles int64 `json:"total_files"` + FilesDone int64 `json:"files_done"` + TotalBytes int64 `json:"total_bytes"` + BytesDone int64 `json:"bytes_done"` + CurrentFiles []string `json:"current_files"` +} - // Error fields - Error string `json:"error"` +func (b *BackupProgressEntry) Validate() error { + if b.MessageType == "summary" && b.SnapshotId != "" { + if err := ValidateSnapshotId(b.SnapshotId); err != nil { + return err + } + } + + return nil } -// readBackupProgressEntrys returns the summary event or an error if the command failed. -func readBackupProgressEntries(cmd *exec.Cmd, output io.Reader, callback func(event *BackupProgressEntry)) (*BackupProgressEntry, error) { +// readBackupProgressEntries returns the summary event or an error if the command failed. +func readBackupProgressEntries(output io.Reader, logger io.Writer, callback func(event *BackupProgressEntry)) (*BackupProgressEntry, error) { scanner := bufio.NewScanner(output) scanner.Split(bufio.ScanLines) - // first event is handled specially to detect non-JSON output and fast-path out. - if scanner.Scan() { - var event BackupProgressEntry - - if err := json.Unmarshal(scanner.Bytes(), &event); err != nil { - var bytes = slices.Clone(scanner.Bytes()) - for scanner.Scan() { - bytes = append(bytes, scanner.Bytes()...) - } - - return nil, NewCmdError(cmd, bytes, fmt.Errorf("command output was not JSON: %w", err)) - } - } + nonJSONOutput := bytes.NewBuffer(nil) - // remaining events are parsed as JSON var summary *BackupProgressEntry + // remaining events are parsed as JSON for scanner.Scan() { - var event *BackupProgressEntry + var event BackupProgressEntry if err := json.Unmarshal(scanner.Bytes(), &event); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) + nonJSONOutput.Write(scanner.Bytes()) + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + continue + } + if err := event.Validate(); err != nil { + nonJSONOutput.Write(scanner.Bytes()) + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + continue + } + if event.MessageType == "error" && logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) } - if callback != nil { - callback(event) + callback(&event) } - if event.MessageType == "summary" { - summary = event + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + summary = &event } } - if err := scanner.Err(); err != nil { - return summary, fmt.Errorf("scanner encountered error: %w", err) + return summary, newErrorWithOutput(err, nonJSONOutput.String()) + } + if summary == nil { + return nil, newErrorWithOutput(errors.New("no summary event found"), nonJSONOutput.String()) } - return summary, nil } +type LsEntry struct { + Name string `json:"name"` + Type string `json:"type"` + Path string `json:"path"` + Uid int64 `json:"uid"` + Gid int64 `json:"gid"` + Size int64 `json:"size"` + Mode int64 `json:"mode"` + Mtime string `json:"mtime"` + Atime string `json:"atime"` + Ctime string `json:"ctime"` +} + +func (e *LsEntry) ToProto() *v1.LsEntry { + return &v1.LsEntry{ + Name: e.Name, + Type: e.Type, + Path: e.Path, + Uid: int64(e.Uid), + Gid: int64(e.Gid), + Size: int64(e.Size), + Mode: int64(e.Mode), + Mtime: e.Mtime, + Atime: e.Atime, + Ctime: e.Ctime, + } +} + func readLs(output io.Reader) (*Snapshot, []*LsEntry, error) { scanner := bufio.NewScanner(output) scanner.Split(bufio.ScanLines) @@ -129,4 +227,120 @@ func readLs(output io.Reader) (*Snapshot, []*LsEntry, error) { entries = append(entries, entry) } return snapshot, entries, nil -} \ No newline at end of file +} + +type ForgetResult struct { + Keep []Snapshot `json:"keep"` + Remove []Snapshot `json:"remove"` +} + +func (r *ForgetResult) Validate() error { + for _, s := range r.Keep { + if err := ValidateSnapshotId(s.Id); err != nil { + return err + } + } + for _, s := range r.Remove { + if err := ValidateSnapshotId(s.Id); err != nil { + return err + } + } + return nil +} + +type RestoreProgressEntry struct { + MessageType string `json:"message_type"` // "summary" or "status" + SecondsElapsed float64 `json:"seconds_elapsed"` + TotalBytes int64 `json:"total_bytes"` + BytesRestored int64 `json:"bytes_restored"` + TotalFiles int64 `json:"total_files"` + FilesRestored int64 `json:"files_restored"` + PercentDone float64 `json:"percent_done"` +} + +func (e *RestoreProgressEntry) Validate() error { + if e.MessageType != "summary" && e.MessageType != "status" { + return fmt.Errorf("message_type must be 'summary' or 'status', got %v", e.MessageType) + } + return nil +} + +// readRestoreProgressEntries returns the summary event or an error if the command failed. +func readRestoreProgressEntries(output io.Reader, logger io.Writer, callback func(event *RestoreProgressEntry)) (*RestoreProgressEntry, error) { + scanner := bufio.NewScanner(output) + scanner.Split(bufio.ScanLines) + + nonJSONOutput := bytes.NewBuffer(nil) + + var summary *RestoreProgressEntry + + // remaining events are parsed as JSON + for scanner.Scan() { + var event RestoreProgressEntry + if err := json.Unmarshal(scanner.Bytes(), &event); err != nil { + nonJSONOutput.Write(scanner.Bytes()) + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + continue + } + if err := event.Validate(); err != nil { + // skip it. Best effort parsing, restic will return with a non-zero exit code if it fails. + nonJSONOutput.Write(scanner.Bytes()) + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + continue + } + + if event.MessageType == "error" && logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + if callback != nil { + callback(&event) + } + if event.MessageType == "summary" { + if logger != nil { + logger.Write(scanner.Bytes()) + logger.Write([]byte("\n")) + } + summary = &event + } + } + + if err := scanner.Err(); err != nil { + return summary, newErrorWithOutput(err, nonJSONOutput.String()) + } + + if summary == nil { + return nil, newErrorWithOutput(errors.New("no summary event found"), nonJSONOutput.String()) + } + + return summary, nil +} + +func ValidateSnapshotId(id string) error { + if len(id) != 64 { + return fmt.Errorf("restic may be out of date (check with `restic self-upgrade`): snapshot ID must be 64 chars, got %v chars", len(id)) + } + return nil +} + +type RepoStats struct { + TotalSize int64 `json:"total_size"` + TotalUncompressedSize int64 `json:"total_uncompressed_size"` + CompressionRatio float64 `json:"compression_ratio"` + CompressionProgress float64 `json:"compression_progress"` + CompressionSpaceSaving float64 `json:"compression_space_saving"` + TotalBlobCount int64 `json:"total_blob_count"` + SnapshotsCount int64 `json:"snapshots_count"` +} + +type RepoConfig struct { + Version int `json:"version"` + Id string `json:"id"` + ChunkerPolynomial string `json:"chunker_polynomial"` +} diff --git a/pkg/restic/outputs_test.go b/pkg/restic/outputs_test.go index da27506f6..af52d07ad 100644 --- a/pkg/restic/outputs_test.go +++ b/pkg/restic/outputs_test.go @@ -2,18 +2,17 @@ package restic import ( "bytes" - "os/exec" "testing" ) func TestReadBackupProgressEntries(t *testing.T) { t.Parallel() testInput := `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":15} - {"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":166,"dirs_new":0,"dirs_changed":0,"dirs_unmodified":128,"data_blobs":0,"tree_blobs":0,"data_added":0,"total_files_processed":166,"total_bytes_processed":16754463,"total_duration":0.235433378,"snapshot_id":"bca1043e"}` + {"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":166,"dirs_new":0,"dirs_changed":0,"dirs_unmodified":128,"data_blobs":0,"tree_blobs":0,"data_added":0,"total_files_processed":166,"total_bytes_processed":16754463,"total_duration":0.235433378,"snapshot_id":"d4558b360cc1b7966e416e010382ab8feb49d14da7832266832d69a43af10147"}` b := bytes.NewBuffer([]byte(testInput)) - summary, err := readBackupProgressEntries(&exec.Cmd{}, b, func(event *BackupProgressEntry) { + summary, err := readBackupProgressEntries(b, nil, func(event *BackupProgressEntry) { t.Logf("event: %v", event) }) if err != nil { @@ -28,7 +27,7 @@ func TestReadBackupProgressEntries(t *testing.T) { } func TestReadLs(t *testing.T) { - testInput := `{"time":"2023-11-10T19:14:17.053824063-08:00","tree":"3e2918b261948e69602ee9504b8f475bcc7cdc4dcec0b3f34ecdb014287d07b2","paths":["/resticui"],"hostname":"pop-os","username":"dontpanic","uid":1000,"gid":1000,"id":"db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52","short_id":"db155169","struct_type":"snapshot"} + testInput := `{"time":"2023-11-10T19:14:17.053824063-08:00","tree":"3e2918b261948e69602ee9504b8f475bcc7cdc4dcec0b3f34ecdb014287d07b2","paths":["/backrest"],"hostname":"pop-os","username":"dontpanic","uid":1000,"gid":1000,"id":"db155169d788e6e432e320aedbdff5a54cc439653093bb56944a67682528aa52","short_id":"db155169","struct_type":"snapshot"} {"name":".git","type":"dir","path":"/.git","uid":1000,"gid":1000,"mode":2147484157,"mtime":"2023-11-10T18:32:38.156599473-08:00","atime":"2023-11-10T18:32:38.156599473-08:00","ctime":"2023-11-10T18:32:38.156599473-08:00","struct_type":"node"} {"name":".gitignore","type":"file","path":"/.gitignore","uid":1000,"gid":1000,"size":22,"mode":436,"mtime":"2023-11-10T00:41:26.611346634-08:00","atime":"2023-11-10T00:41:26.611346634-08:00","ctime":"2023-11-10T00:41:26.611346634-08:00","struct_type":"node"} {"name":"README.md","type":"file","path":"/README.md","uid":1000,"gid":1000,"size":762,"mode":436,"mtime":"2023-11-10T00:59:06.842538768-08:00","atime":"2023-11-10T00:59:06.842538768-08:00","ctime":"2023-11-10T00:59:06.842538768-08:00","struct_type":"node"}` @@ -45,4 +44,4 @@ func TestReadLs(t *testing.T) { if len(entries) != 3 { t.Errorf("wanted 3 entries, got: %d", len(entries)) } -} \ No newline at end of file +} diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 647226171..5ba2cd87b 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -9,250 +9,461 @@ import ( "io" "os" "os/exec" + "slices" + "strings" "sync" - v1 "github.com/garethgeorge/resticui/gen/go/v1" - "github.com/hashicorp/go-multierror" + "github.com/djherbis/buffer" + nio "github.com/djherbis/nio/v3" + "github.com/garethgeorge/backrest/internal/ioutil" ) +var errAlreadyInitialized = errors.New("repo already initialized") +var ErrPartialBackup = errors.New("incomplete backup") +var ErrBackupFailed = errors.New("backup failed") +var ErrRestoreFailed = errors.New("restore failed") +var ErrRepoNotFound = errors.New("repo does not exist") + type Repo struct { - mu sync.Mutex cmd string - repo *v1.Repo - initialized bool + uri string - extraArgs []string - extraEnv []string + opts []GenericOption + + exists error + checkExists sync.Once + initialized error // nil or errAlreadyInitialized if initialized, error if initialization failed. + shouldInitialize sync.Once + repoConfig RepoConfig // set by init (which calls Exists) } -func NewRepo(repo *v1.Repo, opts ...GenericOption) *Repo { - opt := &GenericOpts{} - for _, o := range opts { - o(opt) - } +// NewRepo instantiates a new repository. +func NewRepo(resticBin string, uri string, opts ...GenericOption) *Repo { + opts = append(opts, WithEnv("RESTIC_REPOSITORY="+uri)) return &Repo{ - cmd: "restic", // TODO: configurable binary path - repo: repo, - initialized: false, - extraArgs: opt.extraArgs, - extraEnv: opt.extraEnv, + cmd: resticBin, + uri: uri, + opts: opts, } } -func (r *Repo) buildEnv() []string { - env := []string{ - "RESTIC_REPOSITORY=" + r.repo.GetUri(), - "RESTIC_PASSWORD=" + r.repo.GetPassword(), +func (r *Repo) commandWithContext(ctx context.Context, args []string, opts ...GenericOption) *exec.Cmd { + opt := &GenericOpts{} + resolveOpts(opt, r.opts) + resolveOpts(opt, opts) + + fullCmd := append([]string{r.cmd}, args...) + + if len(opt.prefixCmd) > 0 { + fullCmd = append(slices.Clone(opt.prefixCmd), fullCmd...) } - env = append(env, r.extraEnv...) - env = append(env, r.repo.GetEnv()...) - return env -} -// init initializes the repo, the command will be cancelled with the context. -func (r *Repo) init(ctx context.Context) error { - if r.initialized { - return nil + fullCmd = append(fullCmd, opt.extraArgs...) + + cmd := exec.CommandContext(ctx, fullCmd[0], fullCmd[1:]...) + cmd.Env = append(cmd.Env, opt.extraEnv...) + + logger := LoggerFromContext(ctx) + if logger != nil { + sw := &ioutil.SynchronizedWriter{W: logger} + cmd.Stderr = sw + cmd.Stdout = sw + fmt.Fprintf(logger, "\ncommand: %q\n", fullCmd) } - var args = []string{"init", "--json"} - args = append(args, r.extraArgs...) + return cmd +} - cmd := exec.CommandContext(ctx, r.cmd, args...) - cmd.Env = append(cmd.Env, r.buildEnv()...) +func (r *Repo) pipeCmdOutputToWriter(cmd *exec.Cmd, handlers ...io.Writer) { + stdoutHandlers := slices.Clone(handlers) + stderrHandlers := slices.Clone(handlers) - if output, err := cmd.CombinedOutput(); err != nil { - return NewCmdError(cmd, output, err) + if cmd.Stdout != nil { + handlers = append(stdoutHandlers, cmd.Stdout) + } + if cmd.Stderr != nil { + handlers = append(stderrHandlers, cmd.Stderr) } - r.initialized = true - return nil + mw := io.MultiWriter(handlers...) + mw = &ioutil.SynchronizedWriter{W: mw} + cmd.Stdout = mw + cmd.Stderr = mw } -func (r *Repo) Init(ctx context.Context) error { - r.mu.Lock() - defer r.mu.Unlock() - r.initialized = false - return r.init(ctx) +// Exists checks if the repository exists. +// Returns true if exists, false if it does not exist OR an access error occurred. +func (r *Repo) Exists(ctx context.Context, opts ...GenericOption) error { + r.checkExists.Do(func() { + output := bytes.NewBuffer(nil) + cmd := r.commandWithContext(ctx, []string{"cat", "config"}, opts...) + r.pipeCmdOutputToWriter(cmd, output) + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 10 { + err = ErrRepoNotFound + } + r.exists = newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) + } else if err := json.Unmarshal(output.Bytes(), &r.repoConfig); err != nil { + r.exists = newCmdError(ctx, cmd, newErrorWithOutput(fmt.Errorf("command output is not valid JSON: %w", err), output.String())) + } else { + r.exists = nil + } + }) + return r.exists } -func (r *Repo) Backup(ctx context.Context, progressCallback func(*BackupProgressEntry), opts ...BackupOption) (*BackupProgressEntry, error) { - r.mu.Lock() - defer r.mu.Unlock() +// init initializes the repo, the command will be cancelled with the context. +func (r *Repo) init(ctx context.Context, opts ...GenericOption) error { + if r.Exists(ctx, opts...) == nil { + return nil + } + + r.shouldInitialize.Do(func() { + cmd := r.commandWithContext(ctx, []string{"init", "--json"}, opts...) + output := bytes.NewBuffer(nil) + r.pipeCmdOutputToWriter(cmd, output) + + if err := cmd.Run(); err != nil { + if strings.Contains(output.String(), "config file already exists") || strings.Contains(output.String(), "already initialized") { + r.initialized = errAlreadyInitialized + } else { + r.initialized = newCmdError(ctx, cmd, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String()))) + } + } else { + if err := json.Unmarshal(output.Bytes(), &r.repoConfig); err != nil { + r.initialized = newCmdError(ctx, cmd, newErrorWithOutput(fmt.Errorf("command output is not valid JSON: %w", err), output.String())) + } + r.exists = r.initialized + } + }) + + return r.initialized +} - if err := r.init(ctx); err != nil { - return nil, fmt.Errorf("failed to initialize repo: %w", err) +func (r *Repo) Init(ctx context.Context, opts ...GenericOption) error { + if err := r.init(ctx, opts...); err != nil && !errors.Is(err, errAlreadyInitialized) { + return fmt.Errorf("init failed: %w", err) } - - opt := &BackupOpts{} - for _, o := range opts { - o(opt) + return nil +} + +func (r *Repo) Config(ctx context.Context, opts ...GenericOption) (RepoConfig, error) { + if err := r.Exists(ctx, opts...); err != nil { + return RepoConfig{}, err } + return r.repoConfig, nil +} - for _, p := range opt.paths { +func (r *Repo) Backup(ctx context.Context, paths []string, progressCallback func(*BackupProgressEntry), opts ...GenericOption) (*BackupProgressEntry, error) { + for _, p := range paths { if _, err := os.Stat(p); err != nil { return nil, fmt.Errorf("path %s does not exist: %w", p, err) } } - args := []string{"backup", "--json", "--exclude-caches"} - args = append(args, r.extraArgs...) - args = append(args, opt.paths...) - args = append(args, opt.extraArgs...) - - reader, writer := io.Pipe() + args := []string{"backup", "--json"} + args = append(args, paths...) + opts = append(slices.Clone(opts), WithEnv("RESTIC_PROGRESS_FPS=2")) - cmd := exec.CommandContext(ctx, r.cmd, args...) - cmd.Env = append(cmd.Env, r.buildEnv()...) - cmd.Stderr = writer - cmd.Stdout = writer + logger := LoggerFromContext(ctx) + cmdCtx, cancel := context.WithCancel(ctx) + cmdCtx = ContextWithLogger(cmdCtx, nil) // ensure no logger is used + cmd := r.commandWithContext(cmdCtx, args, opts...) - if err := cmd.Start(); err != nil { - return nil, NewCmdError(cmd, nil, err) + // Ensure the command is logged since we're overriding the logger + if logger != nil { + fmt.Fprintf(logger, "command: %q\n", cmd) } - - var wg sync.WaitGroup - var summary *BackupProgressEntry - var cmdErr error + + buf := buffer.New(32 * 1024) // 32KB IO buffer for the realtime event parsing + reader, writer := nio.Pipe(buf) + r.pipeCmdOutputToWriter(cmd, writer) + var readErr error - + var summary *BackupProgressEntry + var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() + defer cancel() var err error - summary, err = readBackupProgressEntries(cmd, reader, progressCallback) + summary, err = readBackupProgressEntries(reader, logger, progressCallback) if err != nil { readErr = fmt.Errorf("processing command output: %w", err) } }() + cmdErr := cmd.Run() + writer.Close() + wg.Wait() + + if cmdErr != nil || readErr != nil { + if cmdErr != nil { + var exitErr *exec.ExitError + if errors.As(cmdErr, &exitErr) { + if exitErr.ExitCode() == 3 { + cmdErr = ErrPartialBackup + } else { + cmdErr = fmt.Errorf("exit code %d: %w", exitErr.ExitCode(), ErrBackupFailed) + } + } + } + return summary, newCmdError(ctx, cmd, errors.Join(cmdErr, readErr)) + } + return summary, nil +} + +func (r *Repo) Restore(ctx context.Context, snapshot string, callback func(*RestoreProgressEntry), opts ...GenericOption) (*RestoreProgressEntry, error) { + opts = append(slices.Clone(opts), WithEnv("RESTIC_PROGRESS_FPS=2")) + + logger := LoggerFromContext(ctx) + cmdCtx, cancel := context.WithCancel(ctx) + cmdCtx = ContextWithLogger(cmdCtx, nil) // ensure no logger is used + cmd := r.commandWithContext(cmdCtx, []string{"restore", "--json", snapshot}, opts...) + if logger != nil { + fmt.Fprintf(logger, "command: %v %v\n", cmd.Path, strings.Join(cmd.Args, " ")) + } + buf := buffer.New(32 * 1024) // 32KB IO buffer for the realtime event parsing + reader, writer := nio.Pipe(buf) + r.pipeCmdOutputToWriter(cmd, writer) + + var readErr error + var summary *RestoreProgressEntry + var wg sync.WaitGroup wg.Add(1) go func() { - defer writer.Close() defer wg.Done() - if err := cmd.Wait(); err != nil { - cmdErr = NewCmdError(cmd, nil, err) + defer cancel() + var err error + summary, err = readRestoreProgressEntries(reader, logger, callback) + if err != nil { + readErr = fmt.Errorf("processing command output: %w", err) } }() + cmdErr := cmd.Run() + writer.Close() wg.Wait() - - var err error if cmdErr != nil || readErr != nil { - err = multierror.Append(nil, cmdErr, readErr) + if cmdErr != nil { + var exitErr *exec.ExitError + if errors.As(cmdErr, &exitErr) { + if exitErr.ExitCode() == 3 { + cmdErr = ErrPartialBackup + } else { + cmdErr = fmt.Errorf("exit code %d: %w", exitErr.ExitCode(), ErrRestoreFailed) + } + } + } + + return summary, newCmdError(ctx, cmd, errors.Join(cmdErr, readErr)) } - return summary, err + return summary, nil } func (r *Repo) Snapshots(ctx context.Context, opts ...GenericOption) ([]*Snapshot, error) { - r.mu.Lock() - defer r.mu.Unlock() + cmd := r.commandWithContext(ctx, []string{"snapshots", "--json"}, opts...) + output := bytes.NewBuffer(nil) + r.pipeCmdOutputToWriter(cmd, output) - if err := r.init(ctx); err != nil { - return nil, fmt.Errorf("failed to initialize repo: %w", err) + if err := cmd.Run(); err != nil { + return nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } - opt := resolveOpts(opts) + var snapshots []*Snapshot + if err := json.Unmarshal(output.Bytes(), &snapshots); err != nil { + return nil, newCmdError(ctx, cmd, newErrorWithOutput(fmt.Errorf("command output is not valid JSON: %w", err), output.String())) + } - args := []string{"snapshots", "--json"} - args = append(args, r.extraArgs...) - args = append(args, opt.extraArgs...) + for _, snapshot := range snapshots { + if err := snapshot.Validate(); err != nil { + return nil, fmt.Errorf("invalid snapshot: %w", err) + } + } + return snapshots, nil +} - cmd := exec.CommandContext(ctx, r.cmd, args...) - cmd.Env = append(cmd.Env, r.buildEnv()...) - cmd.Env = append(cmd.Env, opt.extraEnv...) +func (r *Repo) Forget(ctx context.Context, policy *RetentionPolicy, opts ...GenericOption) (*ForgetResult, error) { + args := []string{"forget", "--json"} + args = append(args, policy.toForgetFlags()...) - output, err := cmd.CombinedOutput() - if err != nil { - return nil, NewCmdError(cmd, output, err) + cmd := r.commandWithContext(ctx, args, opts...) + output := bytes.NewBuffer(nil) + r.pipeCmdOutputToWriter(cmd, output) + if err := cmd.Run(); err != nil { + return nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } - var snapshots []*Snapshot - if err := json.Unmarshal(output, &snapshots); err != nil { - return nil, NewCmdError(cmd, output, fmt.Errorf("command output is not valid JSON: %w", err)) + var result []ForgetResult + if err := json.Unmarshal(output.Bytes(), &result); err != nil { + return nil, newCmdError(ctx, cmd, newErrorWithOutput(fmt.Errorf("command output is not valid JSON: %w", err), output.String())) + } + if len(result) != 1 { + return nil, fmt.Errorf("expected 1 output from forget, got %v", len(result)) + } + if err := result[0].Validate(); err != nil { + return nil, newCmdError(ctx, cmd, fmt.Errorf("invalid forget result: %w", err)) } - return snapshots, nil + return &result[0], nil } -func (r *Repo) ListDirectory(ctx context.Context, snapshot string, path string, opts ...GenericOption) (*Snapshot, []*LsEntry, error) { - r.mu.Lock() - defer r.mu.Unlock() +func (r *Repo) ForgetSnapshot(ctx context.Context, snapshotId string, opts ...GenericOption) error { + args := []string{"forget", "--json", snapshotId} - if path == "" { - // an empty path can trigger very expensive operations (e.g. iterates all files in the snapshot) - return nil, nil, errors.New("path must not be empty") + output := bytes.NewBuffer(nil) + cmd := r.commandWithContext(ctx, args, opts...) + r.pipeCmdOutputToWriter(cmd, output) + if err := cmd.Run(); err != nil { + return newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } - if err := r.init(ctx); err != nil { - return nil, nil, fmt.Errorf("failed to initialize repo: %w", err) + return nil +} + +func (r *Repo) Prune(ctx context.Context, pruneOutput io.Writer, opts ...GenericOption) error { + args := []string{"prune"} + cmd := r.commandWithContext(ctx, args, opts...) + if pruneOutput != nil { + r.pipeCmdOutputToWriter(cmd, pruneOutput) + } + if err := cmd.Run(); err != nil { + return newCmdError(ctx, cmd, err) } + return nil +} - opt := resolveOpts(opts) +func (r *Repo) Check(ctx context.Context, checkOutput io.Writer, opts ...GenericOption) error { + args := []string{"check"} + cmd := r.commandWithContext(ctx, args, opts...) + cmd.Stdin = bytes.NewBuffer(nil) + if checkOutput != nil { + r.pipeCmdOutputToWriter(cmd, checkOutput) + } + if err := cmd.Run(); err != nil { + return newCmdError(ctx, cmd, err) + } + return nil +} - args := []string{"ls", "--json", snapshot, path} - args = append(args, r.extraArgs...) - args = append(args, opt.extraArgs...) +func (r *Repo) ListDirectory(ctx context.Context, snapshot string, path string, opts ...GenericOption) (*Snapshot, []*LsEntry, error) { + if path == "" { + // an empty path can trigger very expensive operations (e.g. iterates all files in the snapshot) + return nil, nil, errors.New("path must not be empty") + } - cmd := exec.CommandContext(ctx, r.cmd, args...) - cmd.Env = append(cmd.Env, r.buildEnv()...) - cmd.Env = append(cmd.Env, opt.extraEnv...) + cmd := r.commandWithContext(ctx, []string{"ls", "--json", snapshot, path}, opts...) + output := bytes.NewBuffer(nil) + r.pipeCmdOutputToWriter(cmd, output) - output, err := cmd.CombinedOutput() - if err != nil { - return nil, nil, NewCmdError(cmd, output, err) + if err := cmd.Run(); err != nil { + return nil, nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } - - snapshots, entries, err := readLs(bytes.NewBuffer(output)) + snapshots, entries, err := readLs(output) if err != nil { - return nil, nil, NewCmdError(cmd, output, err) + return nil, nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } return snapshots, entries, nil } -type BackupOpts struct { - paths []string - extraArgs []string +func (r *Repo) Unlock(ctx context.Context, opts ...GenericOption) error { + output := bytes.NewBuffer(nil) + cmd := r.commandWithContext(ctx, []string{"unlock"}, opts...) + r.pipeCmdOutputToWriter(cmd, output) + if err := cmd.Run(); err != nil { + return newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) + } + return nil } -type BackupOption func(opts *BackupOpts) +func (r *Repo) Stats(ctx context.Context, opts ...GenericOption) (*RepoStats, error) { + cmd := r.commandWithContext(ctx, []string{"stats", "--json", "--mode=raw-data"}, opts...) + output := bytes.NewBuffer(nil) + r.pipeCmdOutputToWriter(cmd, output) -func WithBackupPaths(paths ...string) BackupOption { - return func(opts *BackupOpts) { - opts.paths = append(opts.paths, paths...) + if err := cmd.Run(); err != nil { + return nil, newCmdError(ctx, cmd, err) } + + var stats RepoStats + if err := json.Unmarshal(output.Bytes(), &stats); err != nil { + return nil, newCmdError(ctx, cmd, newErrorWithOutput(fmt.Errorf("command output is not valid JSON: %w", err), output.String())) + } + + return &stats, nil } -func WithBackupExcludes(excludes ...string) BackupOption { - return func(opts *BackupOpts) { - for _, exclude := range excludes { - opts.extraArgs = append(opts.extraArgs, "--exclude", exclude) - } +// AddTags adds tags to the specified snapshots. +func (r *Repo) AddTags(ctx context.Context, snapshotIDs []string, tags []string, opts ...GenericOption) error { + args := []string{"tag"} + args = append(args, "--add", strings.Join(tags, ",")) + args = append(args, snapshotIDs...) + + cmd := r.commandWithContext(ctx, args, opts...) + if err := cmd.Run(); err != nil { + return newCmdError(ctx, cmd, err) } + return nil } -func WithBackupTags(tags ...string) BackupOption { - return func(opts *BackupOpts) { - for _, tag := range tags { - opts.extraArgs = append(opts.extraArgs, "--tag", tag) - } +func (r *Repo) GenericCommand(ctx context.Context, args []string, opts ...GenericOption) error { + cmd := r.commandWithContext(ctx, args, opts...) + if err := cmd.Run(); err != nil { + return err } + return nil +} + +type RetentionPolicy struct { + KeepLastN int // keep the last n snapshots. + KeepHourly int // keep the last n hourly snapshots. + KeepDaily int // keep the last n daily snapshots. + KeepWeekly int // keep the last n weekly snapshots. + KeepMonthly int // keep the last n monthly snapshots. + KeepYearly int // keep the last n yearly snapshots. + KeepWithinDuration string // keep snapshots within a duration e.g. 1y2m3d4h5m6s +} + +func (r *RetentionPolicy) toForgetFlags() []string { + flags := []string{} + if r.KeepLastN != 0 { + flags = append(flags, "--keep-last", fmt.Sprintf("%d", r.KeepLastN)) + } + if r.KeepHourly != 0 { + flags = append(flags, "--keep-hourly", fmt.Sprintf("%d", r.KeepHourly)) + } + if r.KeepDaily != 0 { + flags = append(flags, "--keep-daily", fmt.Sprintf("%d", r.KeepDaily)) + } + if r.KeepWeekly != 0 { + flags = append(flags, "--keep-weekly", fmt.Sprintf("%d", r.KeepWeekly)) + } + if r.KeepMonthly != 0 { + flags = append(flags, "--keep-monthly", fmt.Sprintf("%d", r.KeepMonthly)) + } + if r.KeepYearly != 0 { + flags = append(flags, "--keep-yearly", fmt.Sprintf("%d", r.KeepYearly)) + } + if r.KeepWithinDuration != "" { + flags = append(flags, "--keep-within", r.KeepWithinDuration) + } + return flags } type GenericOpts struct { extraArgs []string - extraEnv []string + extraEnv []string + prefixCmd []string } -func resolveOpts(opts []GenericOption) *GenericOpts { - opt := &GenericOpts{} +func resolveOpts(opt *GenericOpts, opts []GenericOption) { for _, o := range opts { o(opt) } - return opt } type GenericOption func(opts *GenericOpts) @@ -277,15 +488,31 @@ func WithEnv(env ...string) GenericOption { } } -var EnvToPropagate = []string{"PATH", "HOME", "XDG_CACHE_HOME"} +var EnvToPropagate = []string{ + // *nix systems + "PATH", "HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", + // windows + "APPDATA", "LOCALAPPDATA", +} + func WithPropagatedEnvVars(extras ...string) GenericOption { var extension []string for _, env := range EnvToPropagate { if val, ok := os.LookupEnv(env); ok { - extension = append(extension, env + "=" + val) + extension = append(extension, env+"="+val) } } return WithEnv(extension...) -} \ No newline at end of file +} + +func WithEnviron() GenericOption { + return WithEnv(os.Environ()...) +} + +func WithPrefixCommand(args ...string) GenericOption { + return func(opts *GenericOpts) { + opts.prefixCmd = append(opts.prefixCmd, args...) + } +} diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index f8541727a..26cb7ccc5 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -1,25 +1,78 @@ package restic import ( + "bytes" "context" + "errors" "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "slices" + "strings" "testing" + "time" - v1 "github.com/garethgeorge/resticui/gen/go/v1" - test "github.com/garethgeorge/resticui/internal/test/helpers" + "github.com/garethgeorge/backrest/test/helpers" ) func TestResticInit(t *testing.T) { t.Parallel() repo := t.TempDir() - r := NewRepo(&v1.Repo{ - Id: "test", - Uri: repo, - Password: "test", - }, WithFlags("--no-cache")) + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) - r.init(context.Background()) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } +} + +func TestResticExists(t *testing.T) { + t.Parallel() + repo := t.TempDir() + + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Exists(context.Background()); err == nil { + t.Fatalf("expected repo not to exist") + } + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + r2 := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r2.Exists(context.Background()); err != nil { + t.Fatalf("expected repo to exist, got error: %v", err) + } +} + +func TestResticConfig(t *testing.T) { + t.Parallel() + repo := t.TempDir() + + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Exists(context.Background()); err == nil { + t.Fatalf("expected repo not to exist") + } + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + r2 := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r2.Exists(context.Background()); err != nil { + t.Fatalf("expected repo to exist, got error: %v", err) + } + cfg, err := r2.Config(context.Background()) + if err != nil { + t.Fatalf("failed to get repo config: %v", err) + } + if cfg.Id == "" { + t.Errorf("expected repo id to be set, got: %s", cfg.Id) + } + if cfg.ChunkerPolynomial == "" { + t.Errorf("expected chunker polynomial to be set, got: %s", cfg.ChunkerPolynomial) + } + if cfg.Version == 0 { + t.Errorf("expected version to be set, got: %d", cfg.Version) + } } func TestResticBackup(t *testing.T) { @@ -27,52 +80,94 @@ func TestResticBackup(t *testing.T) { repo := t.TempDir() // create a new repo with cache disabled for testing - r := NewRepo(&v1.Repo{ - Id: "test", - Uri: repo, - Password: "test", - }, WithFlags("--no-cache")) - - testData := test.CreateTestData(t) - testData2 := test.CreateTestData(t) + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + if err := r.Exists(context.Background()); err != nil { + t.Fatalf("expected repo to exist, got error: %v", err) + } + + testData := helpers.CreateTestData(t) + testData2 := helpers.CreateTestData(t) + testDataUnreadable := t.TempDir() + helpers.CreateUnreadable(t, testDataUnreadable+"/unreadable") var tests = []struct { - name string - opts []BackupOption - files int // expected files at the end of the backup - wantErr bool + name string + opts []GenericOption + paths []string + files int64 // expected files at the end of the backup + wantErr bool + unixOnly bool }{ { - name: "no options", - opts: []BackupOption{WithBackupPaths(testData)}, + name: "no options", + paths: []string{testData}, + opts: []GenericOption{}, files: 100, }, { - name: "with two paths", - opts:[]BackupOption{WithBackupPaths(testData), WithBackupPaths(testData2)}, + name: "with two paths", + paths: []string{testData, testData2}, + opts: []GenericOption{}, files: 200, }, { - name: "with exclude", - opts: []BackupOption{WithBackupPaths(testData), WithBackupExcludes("file1*")}, + name: "with exclude", + paths: []string{testData}, + opts: []GenericOption{WithFlags("--exclude", "file1*")}, files: 90, }, { - name: "with exclude pattern", - opts: []BackupOption{WithBackupPaths(testData), WithBackupExcludes("file*")}, + name: "with exclude pattern", + paths: []string{testData}, + opts: []GenericOption{WithFlags("--iexclude=file*")}, files: 0, }, { - name: "with nothing to backup", - opts: []BackupOption{}, + name: "with nothing to backup", + paths: []string{}, + opts: []GenericOption{}, + wantErr: true, + }, + { + name: "with unreadable file", + paths: []string{testData, testDataUnreadable}, + opts: []GenericOption{}, + wantErr: true, + }, + { + name: "with wrapper process", + paths: []string{testData}, + opts: []GenericOption{ + WithPrefixCommand("nice", "-n", "19"), + }, + files: 100, + unixOnly: true, + }, + { + name: "with invalid wrapper process", + paths: []string{testData}, + opts: []GenericOption{ + WithPrefixCommand("invalid-wrapper"), + }, wantErr: true, }, } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { - summary, err := r.Backup(context.Background(), func(event *BackupProgressEntry) { + t.Parallel() + if runtime.GOOS == "windows" && tc.unixOnly { + t.Skip("test is unix only") + } + + gotEvent := false + summary, err := r.Backup(context.Background(), tc.paths, func(event *BackupProgressEntry) { t.Logf("backup event: %v", event) + gotEvent = true }, tc.opts...) if (err != nil) != tc.wantErr { t.Fatalf("wanted error: %v, got: %v", tc.wantErr, err) @@ -89,49 +184,118 @@ func TestResticBackup(t *testing.T) { if summary.TotalFilesProcessed != tc.files { t.Errorf("wanted %d files, got: %d", tc.files, summary.TotalFilesProcessed) } + + if !gotEvent { + t.Errorf("wanted backup event, got: false") + } }) } } +func TestResticPartialBackup(t *testing.T) { + t.Parallel() + repo := t.TempDir() + + // create a new repo with cache disabled for testing + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testDataUnreadable := t.TempDir() + unreadablePath := filepath.Join(testDataUnreadable, "unreadable") + helpers.CreateUnreadable(t, unreadablePath) + + var entries []BackupProgressEntry + + summary, err := r.Backup(context.Background(), []string{testDataUnreadable}, func(entry *BackupProgressEntry) { + entries = append(entries, *entry) + }) + if !errors.Is(err, ErrPartialBackup) { + t.Fatalf("wanted error to be partial backup, got: %v", err) + } + if summary == nil { + t.Fatalf("wanted summary, got: nil") + } + + if !slices.ContainsFunc(entries, func(e BackupProgressEntry) bool { + return e.MessageType == "error" && strings.Contains(e.Item, unreadablePath) + }) { + t.Errorf("wanted entries to contain an error event for the unreadable file (%s), but did not find it", unreadablePath) + t.Logf("entries:\n") + for _, entry := range entries { + t.Logf("%+v\n", entry) + } + } +} + +func TestResticBackupLots(t *testing.T) { + t.Parallel() + t.Skip("this test takes a long time to run") + + repo := t.TempDir() + + // create a new repo with cache disabled for testing + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + // backup 25 times + for i := 0; i < 25; i++ { + _, err := r.Backup(context.Background(), []string{testData}, func(e *BackupProgressEntry) { + t.Logf("backup event: %+v", e) + }) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + } +} + func TestSnapshot(t *testing.T) { t.Parallel() repo := t.TempDir() - r := NewRepo(&v1.Repo{ - Id: "test", - Uri: repo, - Password: "test", - }, WithFlags("--no-cache")) + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } - testData := test.CreateTestData(t) + testData := helpers.CreateTestData(t) for i := 0; i < 10; i++ { - _, err := r.Backup(context.Background(), nil, WithBackupPaths(testData), WithBackupTags(fmt.Sprintf("tag%d", i))) + _, err := r.Backup(context.Background(), []string{testData}, nil, WithFlags("--tag", fmt.Sprintf("tag%d", i))) if err != nil { t.Fatalf("failed to backup and create new snapshot: %v", err) } } var tests = []struct { - name string - opts []GenericOption - count int + name string + opts []GenericOption + count int + checkSnapshotFields bool }{ { - name: "no options", - opts: []GenericOption{}, + name: "no options", + opts: []GenericOption{}, count: 10, }, { - name: "with tag", - opts: []GenericOption{WithTags("tag1")}, - count: 1, + name: "with tag", + opts: []GenericOption{WithTags("tag1")}, + count: 1, + checkSnapshotFields: true, }, } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() snapshots, err := r.Snapshots(context.Background(), tc.opts...) if err != nil { t.Fatalf("failed to list snapshots: %v", err) @@ -140,28 +304,73 @@ func TestSnapshot(t *testing.T) { if len(snapshots) != tc.count { t.Errorf("wanted %d snapshots, got: %d", tc.count, len(snapshots)) } + + // Ensure that snapshot timestamps are set, this is critical for correct ordering in the orchestrator. + for _, snapshot := range snapshots { + if snapshot.UnixTimeMs() == 0 { + t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) + } + if snapshot.SnapshotSummary.DurationMs() == 0 { + t.Errorf("wanted snapshot duration to be non-zero, got: %v", snapshot.SnapshotSummary.DurationMs()) + } + if tc.checkSnapshotFields { + checkSnapshotFieldsHelper(t, snapshot) + } + } }) } } +func checkSnapshotFieldsHelper(t *testing.T, snapshot *Snapshot) { + if snapshot.Id == "" { + t.Errorf("wanted snapshot ID to be non-empty, got: %v", snapshot.Id) + } + if snapshot.Tree == "" { + t.Errorf("wanted snapshot tree to be non-empty, got: %v", snapshot.Tree) + } + if snapshot.Hostname == "" { + t.Errorf("wanted snapshot hostname to be non-empty, got: %v", snapshot.Hostname) + } + if snapshot.Username == "" { + t.Errorf("wanted snapshot username to be non-empty, got: %v", snapshot.Username) + } + if len(snapshot.Paths) == 0 { + t.Errorf("wanted snapshot paths to be non-empty, got: %v", snapshot.Paths) + } + if len(snapshot.Tags) == 0 { + t.Errorf("wanted snapshot tags to be non-empty, got: %v", snapshot.Tags) + } + if snapshot.UnixTimeMs() == 0 { + t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) + } + if snapshot.SnapshotSummary.TotalFilesProcessed == 0 { + t.Errorf("wanted snapshot total files processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalFilesProcessed) + } + if snapshot.SnapshotSummary.TotalBytesProcessed == 0 { + t.Errorf("wanted snapshot total bytes processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalBytesProcessed) + } + if snapshot.SnapshotSummary.DurationMs() == 0 { + t.Errorf("wanted snapshot duration to be non-zero, got: %v", snapshot.SnapshotSummary.DurationMs()) + } +} + func TestLs(t *testing.T) { t.Parallel() repo := t.TempDir() - r := NewRepo(&v1.Repo{ - Id: "test", - Uri: repo, - Password: "test", - }, WithFlags("--no-cache")) + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } - testData := test.CreateTestData(t) + testData := helpers.CreateTestData(t) - snapshot, err := r.Backup(context.Background(), nil, WithBackupPaths(testData)) + snapshot, err := r.Backup(context.Background(), []string{testData}, nil) if err != nil { t.Fatalf("failed to backup and create new snapshot: %v", err) } - _, entries, err := r.ListDirectory(context.Background(), snapshot.SnapshotId, testData) + _, entries, err := r.ListDirectory(context.Background(), snapshot.SnapshotId, toRepoPath(testData)) if err != nil { t.Fatalf("failed to list directory: %v", err) @@ -170,4 +379,295 @@ func TestLs(t *testing.T) { if len(entries) != 101 { t.Errorf("wanted 101 entries, got: %d", len(entries)) } -} \ No newline at end of file +} + +func TestResticForget(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + ids := make([]string, 0) + for i := 0; i < 10; i++ { + output, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + ids = append(ids, output.SnapshotId) + } + + // forget snapshots + res, err := r.Forget(context.Background(), &RetentionPolicy{KeepLastN: 3}) + if err != nil { + t.Fatalf("failed to forget snapshots: %v", err) + } + + if len(res.Keep) != 3 { + t.Errorf("wanted 3 snapshots to be kept, got: %d", len(res.Keep)) + } + + if len(res.Remove) != 7 { + t.Errorf("wanted 7 snapshots to be removed, got: %d", len(res.Remove)) + } + + removedIds := make([]string, 0) + for _, snapshot := range res.Remove { + removedIds = append(removedIds, snapshot.Id) + } + slices.Reverse(removedIds) + keptIds := make([]string, 0) + for _, snapshot := range res.Keep { + keptIds = append(keptIds, snapshot.Id) + } + slices.Reverse(keptIds) + + if !reflect.DeepEqual(removedIds, ids[:7]) { + t.Errorf("wanted removed ids to be %v, got: %v", ids[:7], removedIds) + } + + if !reflect.DeepEqual(keptIds, ids[7:]) { + t.Errorf("wanted kept ids to be %v, got: %v", ids[7:], keptIds) + } +} + +func TestForgetSnapshotId(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + ids := make([]string, 0) + for i := 0; i < 5; i++ { + output, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + ids = append(ids, output.SnapshotId) + } + + // forget snapshot by ID + err := r.ForgetSnapshot(context.Background(), ids[0]) + if err != nil { + t.Fatalf("failed to forget snapshots: %v", err) + } + + snapshots, err := r.Snapshots(context.Background()) + if err != nil { + t.Fatalf("failed to list snapshots: %v", err) + } + if len(snapshots) != 4 { + t.Errorf("wanted 4 snapshots, got: %d", len(snapshots)) + } +} + +func TestResticPrune(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + for i := 0; i < 3; i++ { + _, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup: %v", err) + } + } + + // forget recent snapshots + _, err := r.Forget(context.Background(), &RetentionPolicy{KeepLastN: 1}) + if err != nil { + t.Fatalf("failed to forget snapshots: %v", err) + } + + // prune all snapshots + output := bytes.NewBuffer(nil) + if err := r.Prune(context.Background(), output); err != nil { + t.Fatalf("failed to prune snapshots: %v", err) + } + + wantStr := "collecting packs for deletion and repacking" + + if !bytes.Contains(output.Bytes(), []byte(wantStr)) { + t.Errorf("wanted output to contain 'keep 1 snapshots', got: %s", output.String()) + } +} + +func TestResticRestore(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + restorePath := t.TempDir() + + testData := helpers.CreateTestData(t) + dirCount := strings.Count(testData, string(filepath.Separator)) + if runtime.GOOS == "windows" { + // On Windows, the volume name is also included as a dir in the path. + dirCount += 1 + } + + snapshot, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + + // restore all files + summary, err := r.Restore(context.Background(), snapshot.SnapshotId, func(event *RestoreProgressEntry) { + t.Logf("restore event: %v", event) + }, WithFlags("--target", restorePath)) + if err != nil { + t.Fatalf("failed to restore snapshot: %v", err) + } + + // should be 100 files + parent directories. + fileCount := 100 + dirCount + if summary.TotalFiles != int64(fileCount) { + t.Errorf("wanted %d files to be restored, got: %d", fileCount, summary.TotalFiles) + } +} + +func TestResticStats(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + _, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + + // restore all files + stats, err := r.Stats(context.Background()) + if err != nil { + t.Fatalf("failed to get stats: %v", err) + } + if stats.SnapshotsCount != 1 { + t.Errorf("wanted 1 snapshot, got: %d", stats.SnapshotsCount) + } + if stats.TotalSize == 0 { + t.Errorf("wanted non-zero total size, got: %d", stats.TotalSize) + } + if stats.TotalUncompressedSize == 0 { + t.Errorf("wanted non-zero total uncompressed size, got: %d", stats.TotalUncompressedSize) + } + if stats.TotalBlobCount == 0 { + t.Errorf("wanted non-zero total blob count, got: %d", stats.TotalBlobCount) + } +} + +func TestResticCheck(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testData := helpers.CreateTestData(t) + + _, err := r.Backup(context.Background(), []string{testData}, nil) + if err != nil { + t.Fatalf("failed to backup and create new snapshot: %v", err) + } + + // check repo + output := bytes.NewBuffer(nil) + if err := r.Check(context.Background(), output, WithFlags("--read-data")); err != nil { + t.Fatalf("failed to check repo: %v", err) + } + + wantStr := "no errors were found" + if !bytes.Contains(output.Bytes(), []byte(wantStr)) { + t.Errorf("wanted output to contain 'no errors were found', got: %s", output.String()) + } +} + +func toRepoPath(path string) string { + if runtime.GOOS != "windows" { + return path + } + + // On Windows, the temp directory path needs to be converted to a repo path + // for restic to interpret it correctly in restore/snapshot operations. + sepIdx := strings.Index(path, string(filepath.Separator)) + if sepIdx != 2 || path[1] != ':' { + return path + } + return filepath.ToSlash(filepath.Join( + string(filepath.Separator), // leading slash + string(path[0]), // drive volume + path[3:], // path + )) +} + +func BenchmarkBackup(t *testing.B) { + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + workdir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + t.ResetTimer() + + for i := 0; i < t.N; i++ { + _, err := r.Backup(context.Background(), []string{workdir}, func(e *BackupProgressEntry) {}) + if err != nil { + t.Fatalf("failed to backup: %v", err) + } + } +} + +func BenchmarkBackupWithSimulatedCallback(t *testing.B) { + repo := t.TempDir() + r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test")) + if err := r.Init(context.Background()); err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + workdir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + t.ResetTimer() + + for i := 0; i < t.N; i++ { + _, err := r.Backup(context.Background(), []string{workdir}, func(e *BackupProgressEntry) { + time.Sleep(50 * time.Millisecond) // simulate work being done in the callback + }) + if err != nil { + t.Fatalf("failed to backup: %v", err) + } + } +} diff --git a/proto/buf.gen.yaml b/proto/buf.gen.yaml index ca149dd76..d21fa89f1 100644 --- a/proto/buf.gen.yaml +++ b/proto/buf.gen.yaml @@ -1,19 +1,19 @@ -version: v1 +version: v2 +clean: true plugins: - - plugin: go + - local: protoc-gen-go out: ../gen/go opt: - paths=source_relative - - plugin: go-grpc + - local: protoc-gen-go-grpc out: ../gen/go opt: - paths=source_relative - - plugin: grpc-gateway + - local: protoc-gen-connect-go out: ../gen/go opt: - paths=source_relative - - generate_unbound_methods=true - - plugin: grpc-gateway-ts - out: ../gen/ts + - local: protoc-gen-es + out: ../webui/gen/ts opt: - - paths=source_relative \ No newline at end of file + - target=ts diff --git a/proto/buf.yaml b/proto/buf.yaml index 1a5194568..d7ee92027 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -1,4 +1,4 @@ -version: v1 +version: v2 breaking: use: - FILE diff --git a/proto/build.sh b/proto/build.sh deleted file mode 100755 index 0676734db..000000000 --- a/proto/build.sh +++ /dev/null @@ -1,2 +0,0 @@ -#! /bin/bash -buf generate diff --git a/proto/types/value.proto b/proto/types/value.proto new file mode 100644 index 000000000..b5c15bb0a --- /dev/null +++ b/proto/types/value.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package types; + +option go_package = "github.com/garethgeorge/backrest/gen/go/types"; + +message BoolValue { + bool value = 1; +} + +message StringValue { + string value = 1; +} + +message BytesValue { + bytes value = 1; +} + +message StringList { + repeated string values = 1; +} + +message Int64Value { + int64 value = 1; +} + +message Int64List { + repeated int64 values = 1; +} + +message Empty {} \ No newline at end of file diff --git a/proto/update.sh b/proto/update.sh new file mode 100755 index 000000000..a061fe7b8 --- /dev/null +++ b/proto/update.sh @@ -0,0 +1,3 @@ +#! /bin/sh + +buf generate diff --git a/proto/v1/authentication.proto b/proto/v1/authentication.proto new file mode 100644 index 000000000..7951dab01 --- /dev/null +++ b/proto/v1/authentication.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; + +import "v1/config.proto"; +import "types/value.proto"; +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; + +service Authentication { + rpc Login(LoginRequest) returns (LoginResponse) {} + rpc HashPassword(types.StringValue) returns (types.StringValue) {} +} + +message LoginRequest { + string username = 1; + string password = 2; +} + +message LoginResponse { + string token = 1; // JWT token +} diff --git a/proto/v1/config.proto b/proto/v1/config.proto index a34c24df1..11199ae4f 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -2,29 +2,249 @@ syntax = "proto3"; package v1; -option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; +import "google/protobuf/empty.proto"; +import "v1/crypto.proto"; + +message HubConfig { + repeated InstanceInfo instances = 1 [json_name="instances"]; + + message InstanceInfo { + string id = 1 [json_name="id"]; + string secret = 2 [json_name="secret"]; // secret used to authenticate with the hub. + } +} + + +// Config is the top level config object for restic UI. message Config { - int32 version = 1; - string log_dir = 2 [json_name="log_dir"]; + // modification number, used for read-modify-write consistency in the UI. Incremented on every write. + int32 modno = 1 [json_name="modno"]; + int32 version = 6 [json_name="version"]; // version of the config file format. Used to determine when to run migrations. + + // The instance name for the Backrest installation. + // This identifies backups created by this instance and is displayed in the UI. + string instance = 2 [json_name="instance"]; + repeated Repo repos = 3 [json_name="repos"]; repeated Plan plans = 4 [json_name="plans"]; + Auth auth = 5 [json_name="auth"]; + Multihost multihost = 7 [json_name="sync"]; } -message User { - string name = 1; - string password = 2; // plaintext password +message Multihost { + repeated Peer known_hosts = 1 [json_name="knownHosts"]; + repeated Peer authorized_clients = 2 [json_name="authorizedClients"]; + + message Peer { + string instance_id = 1 [json_name="instanceId"]; // instance ID of the peer. + PublicKey public_key = 3 [json_name="publicKey"]; // public key of the peer. If changed, the peer must re-verify the public key. + bool public_key_verified = 4 [json_name="publicKeyVerified"]; // whether the public key is verified. This must be set for a host to authenticate a client. Clients implicitly validate the first key they see on initial connection. + + // Known host only fields + string instance_url = 2 [json_name="instanceUrl"]; // instance URL, required for a known host. Otherwise meaningless. + } } message Repo { - string id = 1 [json_name="id"]; - string uri = 2 [json_name="uri"]; - string password = 3 [json_name="password"]; - repeated string env = 4 [json_name="env"]; + string id = 1 [json_name="id"]; // unique but human readable ID for this repo. + string uri = 2 [json_name="uri"]; // URI of the repo. + string guid = 11 [json_name="guid"]; // a globally unique ID for this repo. Should be derived as the 'id' field in `restic cat config --json`. + string password = 3 [json_name="password"]; // plaintext password + repeated string env = 4 [json_name="env"]; // extra environment variables to set for restic. + repeated string flags = 5 [json_name="flags"]; // extra flags set on the restic command. + PrunePolicy prune_policy = 6 [json_name="prunePolicy"]; // policy for when to run prune. + CheckPolicy check_policy = 9 [json_name="checkPolicy"]; // policy for when to run check. + repeated Hook hooks = 7 [json_name="hooks"]; // hooks to run on events for this repo. + bool auto_unlock = 8 [json_name="autoUnlock"]; // automatically unlock the repo when needed. + bool auto_initialize = 12 [json_name="autoInitialize"]; // whether the repo should be auto-initialized if not found. + CommandPrefix command_prefix = 10 [json_name="commandPrefix"]; // modifiers for the restic commands + repeated string allowed_peer_instance_ids = 100 [json_name="allowedPeers"]; // list of peer instance IDs allowed to access this repo. } message Plan { - string id = 1 [json_name="id"]; - string repo = 2 [json_name="repo"]; - repeated string paths = 4 [json_name="paths"]; -} \ No newline at end of file + string id = 1 [json_name="id"]; // unique but human readable ID for this plan. + string repo = 2 [json_name="repo"]; // ID of the repo to use. + repeated string paths = 4 [json_name="paths"]; // paths to include in the backup. + repeated string excludes = 5 [json_name="excludes"]; // glob patterns to exclude. + repeated string iexcludes = 9 [json_name="iexcludes"]; // case insensitive glob patterns to exclude. + Schedule schedule = 12 [json_name="schedule"]; // schedule for the backup. + RetentionPolicy retention = 7 [json_name="retention"]; // retention policy for snapshots. + repeated Hook hooks = 8 [json_name="hooks"]; // hooks to run on events for this plan. + repeated string backup_flags = 10 [json_name="backup_flags"]; // extra flags to set when running a backup command. + bool skip_if_unchanged = 13 [json_name="skipIfUnchanged"]; // skip the backup if no changes are detected. + reserved 3, 6, 11; // deprecated +} + +message CommandPrefix { + enum IONiceLevel { + IO_DEFAULT = 0; + IO_BEST_EFFORT_LOW = 1; + IO_BEST_EFFORT_HIGH = 2; + IO_IDLE = 3; + } + + enum CPUNiceLevel { + CPU_DEFAULT = 0; + CPU_HIGH = 1; + CPU_LOW = 2; + } + + IONiceLevel io_nice = 1 [json_name="ioNice"]; // ionice level to set. + CPUNiceLevel cpu_nice = 2 [json_name="cpuNice"]; // nice level to set. +} + +message RetentionPolicy { + oneof policy { + int32 policy_keep_last_n = 10 [json_name="policyKeepLastN"]; + TimeBucketedCounts policy_time_bucketed = 11 [json_name="policyTimeBucketed"]; + bool policy_keep_all = 12 [json_name="policyKeepAll"]; + } + + message TimeBucketedCounts { + int32 hourly = 1 [json_name="hourly"]; // keep the last n hourly snapshots. + int32 daily = 2 [json_name="daily"]; // keep the last n daily snapshots. + int32 weekly = 3 [json_name="weekly"]; // keep the last n weekly snapshots. + int32 monthly = 4 [json_name="monthly"]; // keep the last n monthly snapshots. + int32 yearly = 5 [json_name="yearly"]; // keep the last n yearly snapshots. + } +} + +message PrunePolicy { + Schedule schedule = 2 [json_name="schedule"]; + int64 max_unused_bytes = 3 [json_name="maxUnusedBytes"]; // max unused bytes before running prune. + double max_unused_percent = 4 [json_name="maxUnusedPercent"]; // max unused percent before running prune. +} + +message CheckPolicy { + Schedule schedule = 1 [json_name="schedule"]; + + oneof mode { + bool structure_only = 100 [json_name="structureOnly"]; // only check the structure of the repo. No pack data is read. + double read_data_subset_percent = 101 [json_name="readDataSubsetPercent"]; // check a percentage of pack data. + } +} + +message Schedule { + oneof schedule { + bool disabled = 1 [json_name="disabled"]; // disable the schedule. + string cron = 2 [json_name="cron"]; // cron expression describing the schedule. + int32 maxFrequencyDays = 3 [json_name="maxFrequencyDays"]; // max frequency of runs in days. + int32 maxFrequencyHours = 4 [json_name="maxFrequencyHours"]; // max frequency of runs in hours. + } + + enum Clock { + CLOCK_DEFAULT = 0; // same as CLOCK_LOCAL + CLOCK_LOCAL = 1; + CLOCK_UTC = 2; + CLOCK_LAST_RUN_TIME = 3; + } + + Clock clock = 5 [json_name="clock"]; // clock to use for scheduling. +} + +message Hook { + enum Condition { + CONDITION_UNKNOWN = 0; + CONDITION_ANY_ERROR = 1; // error running any operation. + CONDITION_SNAPSHOT_START = 2; // backup started. + CONDITION_SNAPSHOT_END = 3; // backup completed (success or fail). + CONDITION_SNAPSHOT_ERROR = 4; // snapshot failed. + CONDITION_SNAPSHOT_WARNING = 5; // snapshot completed with warnings. + CONDITION_SNAPSHOT_SUCCESS = 6; // snapshot succeeded. + CONDITION_SNAPSHOT_SKIPPED = 7; // snapshot was skipped e.g. due to no changes. + + // prune conditions + CONDITION_PRUNE_START = 100; // prune started. + CONDITION_PRUNE_ERROR = 101; // prune failed. + CONDITION_PRUNE_SUCCESS = 102; // prune succeeded. + + // check conditions + CONDITION_CHECK_START = 200; // check started. + CONDITION_CHECK_ERROR = 201; // check failed. + CONDITION_CHECK_SUCCESS = 202; // check succeeded. + + // forget conditions + CONDITION_FORGET_START = 300; // forget started. + CONDITION_FORGET_ERROR = 301; // forget failed. + CONDITION_FORGET_SUCCESS = 302; // forget succeeded. + } + + enum OnError { + ON_ERROR_IGNORE = 0; + ON_ERROR_CANCEL = 1; // cancels the operation and skips subsequent hooks + ON_ERROR_FATAL = 2; // fails the operation and subsequent hooks. + ON_ERROR_RETRY_1MINUTE = 100; // retry the operation every minute + ON_ERROR_RETRY_10MINUTES = 101; // retry the operation every 10 minutes + ON_ERROR_RETRY_EXPONENTIAL_BACKOFF = 103; // retry the operation with exponential backoff up to 1h max. + } + + repeated Condition conditions = 1 [json_name="conditions"]; + OnError on_error = 2 [json_name="onError"]; + + oneof action { + Command action_command = 100 [json_name="actionCommand"]; + Webhook action_webhook = 101 [json_name="actionWebhook"]; + Discord action_discord = 102 [json_name="actionDiscord"]; + Gotify action_gotify = 103 [json_name="actionGotify"]; + Slack action_slack = 104 [json_name="actionSlack"]; + Shoutrrr action_shoutrrr = 105 [json_name="actionShoutrrr"]; + Healthchecks action_healthchecks = 106 [json_name="actionHealthchecks"]; + } + + message Command { + string command = 1 [json_name="command"]; + } + + message Webhook { + string webhook_url = 1 [json_name="webhookUrl"]; + enum Method { + UNKNOWN = 0; + GET = 1; + POST = 2; + } + Method method = 2 [json_name="method"]; + string template = 100 [json_name="template"]; + } + + message Discord { + string webhook_url = 1 [json_name="webhookUrl"]; + string template = 2 [json_name="template"]; // template for the webhook payload. + } + + message Gotify { + string base_url = 1 [json_name="baseUrl"]; + string token = 3 [json_name="token"]; + string template = 100 [json_name="template"]; // template for the webhook payload. + string title_template = 101 [json_name="titleTemplate"]; // template for the webhook title. + int32 priority = 102 [json_name="priority"]; // priority level for the notification (1-10) + } + + message Slack { + string webhook_url = 1 [json_name="webhookUrl"]; + string template = 2 [json_name="template"]; // template for the webhook payload. + } + + message Shoutrrr { + string shoutrrr_url = 1 [json_name="shoutrrrUrl"]; + string template = 2 [json_name="template"]; + } + + message Healthchecks { + string webhook_url = 1 [json_name="webhookUrl"]; + string template = 2 [json_name="template"]; + } +} + +message Auth { + bool disabled = 1 [json_name="disabled"]; // disable authentication. + repeated User users = 2 [json_name="users"]; // users to allow access to the UI. +} + +message User { + string name = 1 [json_name="name"]; + oneof password { + string password_bcrypt = 2 [json_name="passwordBcrypt"]; + } +} diff --git a/proto/v1/crypto.proto b/proto/v1/crypto.proto new file mode 100644 index 000000000..2ab61d3e1 --- /dev/null +++ b/proto/v1/crypto.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; + +message SignedMessage { + string keyid = 1; // a unique identifier generated as the SHA256 of the public key used to sign the message. + bytes payload = 2; // the payload + bytes signature = 3; // the signature of the payload +} + +message EncryptedMessage { + bytes payload = 1; +} + +message PublicKey { + string keyid = 1 [json_name="keyid"]; // a unique identifier generated as the SHA256 of the public key. + string ed25519 = 2 [json_name="ed25519pub"]; // base64 encoded public key +} + +message PrivateKey { + string keyid = 1 [json_name="keyid"]; // a unique identifier generated as the SHA256 of the public key. + string ed25519 = 2 [json_name="ed25519priv"]; // base64 encoded private key +} diff --git a/proto/v1/events.proto b/proto/v1/events.proto deleted file mode 100644 index 8ff9bfd86..000000000 --- a/proto/v1/events.proto +++ /dev/null @@ -1,33 +0,0 @@ -syntax = "proto3"; - -package v1; - -option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; - -message Event { - // timestamp is the number of milliseconds since the Unix epoch. - int64 timestamp = 1 [json_name="timestamp"]; - - oneof event { - LogEvent log = 3 [json_name="log"]; - BackupStatusEvent backup_status_change = 4 [json_name="backup_status"]; - } - -} - -message LogEvent { - string message = 1 [json_name="message"]; -} - -message BackupStatusEvent { - string plan = 1 [json_name="plan"]; - Status status = 2 [json_name="status"]; - uint32 percent = 3 [json_name="percent"]; -} - -enum Status { - UNKNOWN = 0; - IN_PROGRESS = 1; - SUCCESS = 2; - FAILED = 3; -} \ No newline at end of file diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto new file mode 100644 index 000000000..1a9dda927 --- /dev/null +++ b/proto/v1/operations.proto @@ -0,0 +1,140 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; + +import "v1/restic.proto"; +import "v1/config.proto"; +import "types/value.proto"; + +message OperationList { + repeated Operation operations = 1; +} + +message Operation { + // required, primary ID of the operation. ID is sequential based on creation time of the operation. + int64 id = 1; + int64 original_id = 13; + // modno increments with each change to the operation. This supports easy diffing. + int64 modno = 12; + // flow id groups operations together, e.g. by an execution of a plan. + // must be unique within the context of a repo. + int64 flow_id = 10; + int64 original_flow_id = 14; + // repo id is a string identifier for the repo, and repo_guid is the globally unique ID of the repo. + string repo_id = 2; + string repo_guid = 15; + // plan id e.g. a scheduled set of operations (or system) that created this operation. + string plan_id = 3; + // instance ID that created the operation + string instance_id = 11; + // optional snapshot id if associated with a snapshot. + string snapshot_id = 8; + OperationStatus status = 4; + // required, unix time in milliseconds of the operation's creation (ID is derived from this) + int64 unix_time_start_ms = 5; + // ptional, unix time in milliseconds of the operation's completion + int64 unix_time_end_ms = 6; + // optional, human readable context message, typically an error message. + string display_message = 7; + // logref can point to arbitrary logs associated with the operation. + string logref = 9; + + oneof op { + OperationBackup operation_backup = 100; + OperationIndexSnapshot operation_index_snapshot = 101; + OperationForget operation_forget = 102; + OperationPrune operation_prune = 103; + OperationRestore operation_restore = 104; + OperationStats operation_stats = 105; + OperationRunHook operation_run_hook = 106; + OperationCheck operation_check = 107; + OperationRunCommand operation_run_command = 108; + } +} + +// OperationEvent is used in the wireformat to stream operation changes to clients +message OperationEvent { + oneof event { + types.Empty keep_alive = 1; + OperationList created_operations = 2; + OperationList updated_operations = 3; + types.Int64List deleted_operations = 4; + } +} + +// OperationEventType indicates whether the operation was created or updated +enum OperationEventType { + EVENT_UNKNOWN = 0; + EVENT_CREATED = 1; + EVENT_UPDATED = 2; + EVENT_DELETED = 3; +} + +enum OperationStatus { + STATUS_UNKNOWN = 0; // used to indicate that the status is unknown. + STATUS_PENDING = 1; // used to indicate that the operation is pending. + STATUS_INPROGRESS = 2; // used to indicate that the operation is in progress. + STATUS_SUCCESS = 3; // used to indicate that the operation completed successfully. + STATUS_WARNING = 7; // used to indicate that the operation completed with warnings. + STATUS_ERROR = 4; // used to indicate that the operation failed. + STATUS_SYSTEM_CANCELLED = 5; // indicates operation cancelled by the system. + STATUS_USER_CANCELLED = 6; // indicates operation cancelled by the user. +} + +message OperationBackup { + BackupProgressEntry last_status = 3; + repeated BackupProgressError errors = 4; +} + +// OperationIndexSnapshot tracks that a snapshot was detected by backrest. +message OperationIndexSnapshot { + ResticSnapshot snapshot = 2; // the snapshot that was indexed. + bool forgot = 3; // tracks whether this snapshot is forgotten yet. +} + +// OperationForget tracks a forget operation. +message OperationForget { + repeated ResticSnapshot forget = 1; + RetentionPolicy policy = 2; +} + +// OperationPrune tracks a prune operation. +message OperationPrune { + string output = 1 [deprecated = true]; // output of the prune. + string output_logref = 2; // logref of the prune output. +} + +// OperationCheck tracks a check operation. +message OperationCheck { + string output = 1 [deprecated = true]; // output of the check operation. + string output_logref = 2; // logref of the check output. +} + +// OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. +message OperationRunCommand { + string command = 1; + string output_logref = 2; + int64 output_size_bytes = 3; // not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. +} + +// OperationRestore tracks a restore operation. +message OperationRestore { + string path = 1; // path in the snapshot to restore. + string target = 2; // location to restore it to. + RestoreProgressEntry last_status = 3; // status of the restore. +} + +// OperationStats tracks a stats operation. +message OperationStats { + RepoStats stats = 1; +} + +// OperationRunHook tracks a hook that was run. +message OperationRunHook { + int64 parent_op = 4; // ID of the operation that ran the hook. + string name = 1; // description of the hook that was run. typically repo/hook_idx or plan/hook_idx. + string output_logref = 2; // logref of the hook's output. DEPRECATED. + Hook.Condition condition = 3; // triggering condition of the hook. +} diff --git a/proto/v1/restic.proto b/proto/v1/restic.proto new file mode 100644 index 000000000..7c373a1b2 --- /dev/null +++ b/proto/v1/restic.proto @@ -0,0 +1,101 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; + +// ResticSnapshot represents a restic snapshot. +message ResticSnapshot { + string id = 1; + int64 unix_time_ms = 2; + string hostname = 3; + string username = 4; + string tree = 5; // tree hash + string parent = 6; // parent snapshot's id + repeated string paths = 7; + repeated string tags = 8; + SnapshotSummary summary = 9; // added in 0.17.0 restic outputs the summary in the snapshot +} + +message SnapshotSummary { + int64 files_new = 1; + int64 files_changed = 2; + int64 files_unmodified = 3; + int64 dirs_new = 4; + int64 dirs_changed = 5; + int64 dirs_unmodified = 6; + int64 data_blobs = 7; + int64 tree_blobs = 8; + int64 data_added = 9; + int64 total_files_processed = 10; + int64 total_bytes_processed = 11; + double total_duration = 12; +} + +// ResticSnapshotList represents a list of restic snapshots. +message ResticSnapshotList { + repeated ResticSnapshot snapshots = 1; +} + +// BackupProgressEntriy represents a single entry in the backup progress stream. +message BackupProgressEntry { + oneof entry { + BackupProgressStatusEntry status = 1; + BackupProgressSummary summary = 2; + } +} + +// BackupProgressStatusEntry represents a single status entry in the backup progress stream. +message BackupProgressStatusEntry { + // See https://restic.readthedocs.io/en/stable/075_scripting.html#id1 + double percent_done = 1; // 0.0 - 1.0 + int64 total_files = 2; + int64 total_bytes = 3; + int64 files_done = 4; + int64 bytes_done = 5; + repeated string current_file = 6; +} + +// BackupProgressSummary represents a the summary event emitted at the end of a backup stream. +message BackupProgressSummary { + // See https://restic.readthedocs.io/en/stable/075_scripting.html#summary + int64 files_new = 1; + int64 files_changed = 2; + int64 files_unmodified = 3; + int64 dirs_new = 4; + int64 dirs_changed = 5; + int64 dirs_unmodified = 6; + int64 data_blobs = 7; + int64 tree_blobs = 8; + int64 data_added = 9; + int64 total_files_processed = 10; + int64 total_bytes_processed = 11; + double total_duration = 12; + string snapshot_id = 13; +} + +message BackupProgressError { + // See https://restic.readthedocs.io/en/stable/075_scripting.html#error + string item = 1; + string during = 2; + string message = 3; +} + +// RestoreProgressEvent represents a single entry in the restore progress stream. +message RestoreProgressEntry { + string message_type = 1; // "summary" or "status" + double seconds_elapsed = 2; + int64 total_bytes = 3; + int64 bytes_restored = 4; + int64 total_files = 5; + int64 files_restored = 6; + double percent_done = 7; // 0.0 - 1.0 +} + +message RepoStats { + int64 total_size = 1; + int64 total_uncompressed_size = 2; + double compression_ratio = 3; + int64 total_blob_count = 5; + int64 snapshot_count = 6; +} \ No newline at end of file diff --git a/proto/v1/service.proto b/proto/v1/service.proto index db70701fb..ab5e11083 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -2,29 +2,181 @@ syntax = "proto3"; package v1; -option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; import "v1/config.proto"; -import "v1/events.proto"; +import "v1/restic.proto"; +import "v1/operations.proto"; +import "types/value.proto"; import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; -service ResticUI { - rpc GetConfig (google.protobuf.Empty) returns (Config) { - option (google.api.http) = { - get: "/v1/config" - }; +service Backrest { + rpc GetConfig (google.protobuf.Empty) returns (Config) {} + + rpc SetConfig (Config) returns (Config) {} + + rpc CheckRepoExists (Repo) returns (types.BoolValue) {} // returns an error if the repo does not exist + + rpc AddRepo (Repo) returns (Config) {} + + rpc RemoveRepo (types.StringValue) returns (Config) {} // remvoes a repo from the config and deletes its history, does not delete the repo on disk + + rpc GetOperationEvents (google.protobuf.Empty) returns (stream OperationEvent) {} + + rpc GetOperations (GetOperationsRequest) returns (OperationList) {} + + rpc ListSnapshots(ListSnapshotsRequest) returns (ResticSnapshotList) {} + + rpc ListSnapshotFiles(ListSnapshotFilesRequest) returns (ListSnapshotFilesResponse) {} + + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + rpc Backup(types.StringValue) returns (google.protobuf.Empty) {} + + // DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + rpc DoRepoTask(DoRepoTaskRequest) returns (google.protobuf.Empty) {} + + // Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + rpc Forget(ForgetRequest) returns (google.protobuf.Empty) {} + + // Restore schedules a restore operation. + rpc Restore(RestoreSnapshotRequest) returns (google.protobuf.Empty) {} + + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) {} + + // GetLogs returns the keyed large data for the given operation. + rpc GetLogs(LogDataRequest) returns (stream types.BytesValue) {} + + // RunCommand executes a generic restic command on the repository. + rpc RunCommand(RunCommandRequest) returns (types.Int64Value) {} + + // GetDownloadURL returns a signed download URL given a forget operation ID. + rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {} + + // Clears the history of operations + rpc ClearHistory(ClearHistoryRequest) returns (google.protobuf.Empty) {} + + // PathAutocomplete provides path autocompletion options for a given filesystem path. + rpc PathAutocomplete (types.StringValue) returns (types.StringList) {} + + // GetSummaryDashboard returns data for the dashboard view. + rpc GetSummaryDashboard(google.protobuf.Empty) returns (SummaryDashboardResponse) {} +} + +// OpSelector is a message that can be used to select operations e.g. by query. +message OpSelector { + repeated int64 ids = 1; + optional string instance_id = 6; + optional string repo_guid = 7; + optional string plan_id = 3; + optional string snapshot_id = 4; + optional int64 flow_id = 5; +} + +message DoRepoTaskRequest { + string repo_id = 1; + enum Task { + TASK_NONE = 0; + TASK_INDEX_SNAPSHOTS = 1; + TASK_PRUNE = 2; + TASK_CHECK = 3; + TASK_STATS = 4; + TASK_UNLOCK = 5; } - rpc SetConfig (Config) returns (Config) { - option (google.api.http) = { - post: "/v1/config" - body: "*" - }; + Task task = 2; +} + +message ClearHistoryRequest { + OpSelector selector = 1; + bool only_failed = 2; +} + +message ForgetRequest { + string repo_id = 1; + string plan_id = 2; + string snapshot_id = 3; +} + +message ListSnapshotsRequest { + string repo_id = 1; + string plan_id = 2; +} + +message GetOperationsRequest { + OpSelector selector = 1; + int64 last_n = 2; // limit to the last n operations +} + +message RestoreSnapshotRequest { + string plan_id = 1; + string repo_id = 5; + string snapshot_id = 2; + string path = 3; + string target = 4; +} + +message ListSnapshotFilesRequest { + string repo_id = 1; + string snapshot_id = 2; + string path = 3; +} + +message ListSnapshotFilesResponse { + string path = 1; + repeated LsEntry entries = 2; +} + +message LogDataRequest { + string ref = 1; +} + +message LsEntry { + string name = 1; + string type = 2; + string path = 3; + int64 uid = 4; + int64 gid = 5; + int64 size = 6; + int64 mode = 7; + string mtime = 8; + string atime = 9; + string ctime = 10; +} + +message RunCommandRequest { + string repo_id = 1; + string command = 2; +} + +message SummaryDashboardResponse { + repeated Summary repo_summaries = 1; + repeated Summary plan_summaries = 2; + + string config_path = 10; + string data_path = 11; + + message Summary { + string id = 1; + int64 backups_failed_30days = 2; + int64 backups_warning_last_30days = 3; + int64 backups_success_last_30days = 4; + int64 bytes_scanned_last_30days = 5; + int64 bytes_added_last_30days = 6; + int64 total_snapshots = 7; + int64 bytes_scanned_avg = 8; + int64 bytes_added_avg = 9; + int64 next_backup_time_ms = 10; + + // Charts + BackupChart recent_backups = 11; // recent backups } - rpc GetEvents (google.protobuf.Empty) returns (stream Event) { - option (google.api.http) = { - get: "/v1/events" - }; + message BackupChart { + repeated int64 flow_id = 1; + repeated int64 timestamp_ms = 2; + repeated int64 duration_ms = 3; + repeated OperationStatus status = 4; + repeated int64 bytes_added = 5; } } diff --git a/proto/v1/syncservice.proto b/proto/v1/syncservice.proto new file mode 100644 index 000000000..0f08a74a5 --- /dev/null +++ b/proto/v1/syncservice.proto @@ -0,0 +1,113 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; + +import "v1/config.proto"; +import "v1/crypto.proto"; +import "v1/restic.proto"; +import "v1/service.proto"; +import "v1/operations.proto"; +import "types/value.proto"; +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; + + +service BackrestSyncService { + rpc Sync(stream SyncStreamItem) returns (stream SyncStreamItem) {} + rpc GetRemoteRepos(google.protobuf.Empty) returns (GetRemoteReposResponse) {} +} + +message GetRemoteReposResponse { + message RemoteRepoMetadata { + string instance_id = 1; + string repo_id = 2; + } + + repeated RemoteRepoMetadata repos = 1; +} + +enum SyncConnectionState { + CONNECTION_STATE_UNKNOWN = 0; + CONNECTION_STATE_PENDING = 1; + CONNECTION_STATE_CONNECTED = 2; + CONNECTION_STATE_DISCONNECTED = 3; + CONNECTION_STATE_RETRY_WAIT = 4; + CONNECTION_STATE_ERROR_AUTH = 10; + CONNECTION_STATE_ERROR_PROTOCOL = 11; +} + +message SyncStreamItem { + oneof action { + SignedMessage signed_message = 1; + SyncActionHandshake handshake = 3; + + SyncActionDiffOperations diff_operations = 20; + SyncActionSendOperations send_operations = 21; + SyncActionSendConfig send_config = 22; + SyncEstablishSharedSecret establish_shared_secret = 23; + + SyncActionThrottle throttle = 1000; + } + + message SyncActionHandshake { + int64 protocol_version = 1; + PublicKey public_key = 2; + SignedMessage instance_id = 3; + } + + message SyncActionSendConfig { + RemoteConfig config = 1; + } + + message SyncActionConnectRepo { + string repo_id = 1; + } + + enum RepoConnectionState { + CONNECTION_STATE_UNKNOWN = 0; + CONNECTION_STATE_PENDING = 1; // queried, response not yet received. + CONNECTION_STATE_CONNECTED = 2; + CONNECTION_STATE_UNAUTHORIZED = 3; + CONNECTION_STATE_NOT_FOUND = 4; + } + + message SyncActionDiffOperations { + // Client connects and sends a list of "have_operations" that exist in its log. + // have_operation_ids and have_operation_modnos are the operation IDs and modnos that the client has when zip'd pairwise. + OpSelector have_operations_selector = 1; + repeated int64 have_operation_ids = 2; + repeated int64 have_operation_modnos = 3; + // Server sends a list of "request_operations" for any operations that it doesn't have. + repeated int64 request_operations = 4; + } + + message SyncActionSendOperations { + OperationEvent event = 1; + } + + message SyncActionThrottle { + int64 delay_ms = 1; + } + + message SyncEstablishSharedSecret { + // a one-time-use ed25519 public key with a matching unshared private key. Used to perform a key exchange. + // See https://pkg.go.dev/crypto/ecdh#PrivateKey.ECDH . + string ed25519 = 2 [json_name="ed25519pub"]; // base64 encoded public key + } +} + +// RemoteConfig contains shareable properties from a remote backrest instance. +message RemoteConfig { + repeated RemoteRepo repos = 1; +} + +message RemoteRepo { + string id = 1; + string guid = 11; + string uri = 2; + string password = 3; + repeated string env = 4; + repeated string flags = 5; +} diff --git a/scripts/generate-installers.sh b/scripts/generate-installers.sh new file mode 100755 index 000000000..efd75fef2 --- /dev/null +++ b/scripts/generate-installers.sh @@ -0,0 +1,20 @@ +#! /bin/bash + +outdir=$(realpath $1) # output directory for the installer binaries +srcdir=$(realpath $(dirname $0)/..) # source directory + +# for each supported windows architecture +for arch in x86_64 arm64; do + cd $(mktemp -d) + unzip $srcdir/dist/backrest_Windows_${arch}.zip + + cp -rl $srcdir/build/windows/* . + + if [ "$arch" == "x86_64" ]; then + docker run --rm -v $(pwd):/build binfalse/nsis install.nsi + else + docker run --rm -e TARGET_ARCH=arm64 -v $(pwd):/build binfalse/nsis install.nsi + fi + + cp Backrest-setup.exe $outdir/Backrest-setup-${arch}.exe +done diff --git a/scripts/latest-restic-version.sh b/scripts/latest-restic-version.sh new file mode 100755 index 000000000..ad5a4717d --- /dev/null +++ b/scripts/latest-restic-version.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +curl -s https://api.github.com/repos/restic/restic/releases/latest \ + | grep "https://api.github.com/repos/restic/restic/tarball/" \ + | sed -E 's/.*v([0-9]+\.[0-9]+\.[0-9]+).*/\1/' # extract just the version number diff --git a/scripts/manage.sh b/scripts/manage.sh new file mode 100755 index 000000000..9eecaab21 --- /dev/null +++ b/scripts/manage.sh @@ -0,0 +1,137 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +#================================================= +# Description: Backrest Helper +# Contributed-By: https://github.com/Nebulosa-Cat +#================================================= + +OS=$(uname -s) +if [[ "${OS}" != "Linux" ]]; then + echo -e "This script is only for Linux" + exit 1 +fi + +# Formatting Variables +Green_font_prefix="\033[32m" +Red_font_prefix="\033[31m" +Green_background_prefix="\033[42;37m" +Red_background_prefix="\033[41;37m" +Font_color_suffix="\033[0m" +Yellow_font_prefix="\033[0;33m" +Info="${Green_font_prefix}[Info]${Font_color_suffix}" +Error="${Red_font_prefix}[Error]${Font_color_suffix}" +Warning="${Yellow_font_prefix}[Warning]${Font_color_suffix}" + +# Check Kernal Type +sysArch() { + echo -e "${Info} Checking OS info..." + uname=$(uname -m) + + if [[ "$uname" == "x86_64" ]]; then + arch="x86_64" + elif [[ "$uname" == "armv7" ]] || [[ "$uname" == "armv6l" ]]; then + arch="armv6" + elif [[ "$uname" == "armv8" ]] || [[ "$uname" == "aarch64" ]]; then + arch="arm64" + else + echo -e "${Error} Unsupported Architecture ${arch}" && exit 1 + fi + + echo -e "${Info} You are running ${arch}." +} + + +Install() { + sysArch + tempdir=$(mktemp -d) + cd "${tempdir}" + echo -e "${Info} Downloading Latest Version..." + wget https://github.com/garethgeorge/backrest/releases/latest/download/backrest_Linux_${arch}.tar.gz + rm -r ./backrest + mkdir ./backrest + tar -xzvf backrest_Linux_${arch}.tar.gz -C ./backrest + cd backrest + echo -e "${Info} Starting Install Script..." + ./install.sh + echo -e "${Info} Clearing temporary directory for install..." + rm -rf ${tempdir} + echo -e "${Info} Install Completed!" +} + +Uninstall(){ + sysArch + tempdir=$(mktemp -d) + cd "${tempdir}" + echo -e "${Info} Downloading Latest Version of uninstaller..." + wget https://github.com/garethgeorge/backrest/releases/latest/download/backrest_Linux_${arch}.tar.gz + rm -r ./backrest + mkdir ./backrest + tar -xzvf backrest_Linux_${arch}.tar.gz -C ./backrest + cd backrest + echo -e "${Info} Starting Uninstall Script..." + ./uninstall.sh + echo -e "${Info} Clear temporary directory for uninstall script..." + rm -rf ${tempdir} + echo -e "${Info} Uninstall Completed!" +} + +Start(){ + sudo systemctl start backrest +} + +Stop(){ + sudo systemctl stop backrest +} + +Status(){ + sudo systemctl status backrest +} + +Start_Menu(){ + clear + echo -e " +============================================= + Backrest Install Helper +============================================= + ${Red_font_prefix} Warning: This Script Only Work On Linux !${Font_color_suffix} +————————————————————————————————------------- + ${Green_font_prefix} 0.${Font_color_suffix} Install / Update Backrest + ${Green_font_prefix} 1.${Font_color_suffix} Uninstall Backrest +—————————————————————————————————------------ + ${Green_font_prefix} 2.${Font_color_suffix} Start Backrest + ${Green_font_prefix} 3.${Font_color_suffix} Stop Backrest +—————————————————————————————————------------ + ${Green_font_prefix} 4.${Font_color_suffix} Show Backrest Status +--------------------------------------------- + ${Green_font_prefix} 9.${Font_color_suffix} Exit Script +============================================= +" + read -p " Please Input [0-9]:" num + case "$num" in + 0) + Install + ;; + 1) + Uninstall + ;; + 2) + Start + ;; + 3) + Stop + ;; + 4) + Status + ;; + 9) + exit 1 + ;; + *) + echo -e "${Error} Please enter a correct number [0-9]" + exit 1 + ;; + esac +} +Start_Menu \ No newline at end of file diff --git a/scripts/testing/ramdisk-mount.sh b/scripts/testing/ramdisk-mount.sh new file mode 100644 index 000000000..173c686cc --- /dev/null +++ b/scripts/testing/ramdisk-mount.sh @@ -0,0 +1,34 @@ +#! /bin/bash + +# Check that the script must be sourced +( + [[ -n $ZSH_VERSION && $ZSH_EVAL_CONTEXT =~ :file$ ]] || + [[ -n $KSH_VERSION && "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ]] || + [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null) +) && sourced=1 || sourced=0 + +if [ $sourced -eq 0 ]; then + echo "This script should be sourced instead of executed." + echo "Usage: . $0" + exit 1 +fi + +# Check if MacOS +if [ "$(uname)" = "Darwin" ]; then + if [ -d "/Volumes/RAM_Disk_1GB" ]; then + echo "RAM disk /Volumes/RAM_Disk_1GB already exists." + else + sudo diskutil erasevolume HFS+ RAM_Disk_1GB $(hdiutil attach -nomount ram://2048000) + fi + export TMPDIR="/Volumes/RAM_Disk_1GB" + export RESTIC_CACHE_DIR="$TMPDIR/.cache" + echo "Created 512MB RAM disk at /Volumes/RAM_Disk_1GB" + echo "TMPDIR=$TMPDIR" + echo "RESTIC_CACHE_DIR=$RESTIC_CACHE_DIR" +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + # Create ramdisk + sudo mkdir -p /mnt/ramdisk + sudo mount -t tmpfs -o size=1024M tmpfs /mnt/ramdisk + export TMPDIR="/mnt/ramdisk" + export RESTIC_CACHE_DIR="$TMPDIR/.cache" +fi diff --git a/scripts/testing/ramdisk-unmount.sh b/scripts/testing/ramdisk-unmount.sh new file mode 100644 index 000000000..da09f5e81 --- /dev/null +++ b/scripts/testing/ramdisk-unmount.sh @@ -0,0 +1,25 @@ +#! /bin/bash + +# Check that the script must be sourced +( + [[ -n $ZSH_VERSION && $ZSH_EVAL_CONTEXT =~ :file$ ]] || + [[ -n $KSH_VERSION && "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ]] || + [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null) +) && sourced=1 || sourced=0 + +if [ $sourced -eq 0 ]; then + echo "This script should be sourced instead of executed." + echo "Usage: . $0" + exit 1 +fi + +# Check if MacOS +if [ "$(uname)" = "Darwin" ]; then + sudo diskutil unmount /Volumes/RAM_Disk_1GB + hdiutil detach /Volumes/RAM_Disk_1GB +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + sudo umount /mnt/ramdisk +fi + +unset TMPDIR +unset XDG_CACHE_HOME diff --git a/scripts/testing/run-fresh.sh b/scripts/testing/run-fresh.sh new file mode 100644 index 000000000..1337350d9 --- /dev/null +++ b/scripts/testing/run-fresh.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +BASEDIR=$(dirname "$0") +TEMPDIR=$(mktemp -d) + +function cleanup { + echo "Removing temp dir: $TEMPDIR" + rm -rf $TEMPDIR +} + +trap cleanup EXIT + +echo "Temp dir: $TEMPDIR" + +go run $BASEDIR/../../cmd/backrest --config-file=$TEMPDIR/config.json --data-dir=$TEMPDIR/data diff --git a/scripts/testing/run-in-dir.sh b/scripts/testing/run-in-dir.sh new file mode 100755 index 000000000..fb1433950 --- /dev/null +++ b/scripts/testing/run-in-dir.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +BASEDIR=$(dirname "$0") +RUNDIR=$1 + +if [ -z "$RUNDIR" ]; then + echo "Usage: $0 " + exit 1 +fi + +go run $BASEDIR/../../cmd/backrest --config-file=$RUNDIR/config.json --data-dir=$RUNDIR + diff --git a/scripts/update-restic-version.sh b/scripts/update-restic-version.sh new file mode 100755 index 000000000..8de9ecc93 --- /dev/null +++ b/scripts/update-restic-version.sh @@ -0,0 +1,12 @@ +#! /bin/bash + +latest_restic_version=$(./scripts/latest-restic-version.sh) + +if [ -z "$latest_restic_version" ]; then + echo "Failed to get latest restic version" + exit 1 +fi + +echo "Latest restic version: $latest_restic_version" + +sed -i -E "s/^.*RequiredResticVersion\ =\ .*$/ RequiredResticVersion\ =\ \"$latest_restic_version\"/g" internal/resticinstaller/resticinstaller.go \ No newline at end of file diff --git a/static/index.html b/static/index.html deleted file mode 100644 index df213b132..000000000 --- a/static/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - ResticUI - - - -

ResticUI

-

Nothing to see here, move along.

- - \ No newline at end of file diff --git a/static/static.go b/static/static.go deleted file mode 100644 index 9845722e0..000000000 --- a/static/static.go +++ /dev/null @@ -1,9 +0,0 @@ -package static - -import ( - "embed" - _ "embed" -) - -//go:embed * -var FS embed.FS \ No newline at end of file diff --git a/test/helpers/installrestic.go b/test/helpers/installrestic.go new file mode 100644 index 000000000..301839d22 --- /dev/null +++ b/test/helpers/installrestic.go @@ -0,0 +1,15 @@ +package helpers + +import ( + "testing" + + "github.com/garethgeorge/backrest/internal/resticinstaller" +) + +func ResticBinary(t testing.TB) string { + binPath, err := resticinstaller.FindOrInstallResticBinary() + if err != nil { + t.Fatalf("find restic binary: %v", err) + } + return binPath +} diff --git a/test/helpers/testdata.go b/test/helpers/testdata.go new file mode 100644 index 000000000..b44044c1f --- /dev/null +++ b/test/helpers/testdata.go @@ -0,0 +1,37 @@ +package helpers + +import ( + "fmt" + "os" + "path" + "testing" + + acl "github.com/hectane/go-acl" +) + +func CreateTestData(t *testing.T) string { + t.Helper() + dir := t.TempDir() + + for i := 0; i < 100; i++ { + err := os.WriteFile(path.Join(dir, fmt.Sprintf("file%2d", i)), []byte(fmt.Sprintf("test data %d", i)), 0644) + if err != nil { + t.Fatalf("failed to create test data: %v", err) + } + } + return dir +} + +func CreateUnreadable(t *testing.T, path string) { + t.Helper() + + // Create a file that can be written but can't be read by the current user + err := os.WriteFile(path, []byte("test data"), 0200) + if err != nil { + t.Fatalf("failed to create unreadable file: %v", err) + } + + if err := acl.Chmod(path, 0200); err != nil { + t.Fatalf("failed to set file ACL: %v", err) + } +} diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 000000000..f28452f3b --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,48 @@ +#! /bin/bash + +cd "$(dirname "$0")" # cd to the directory of this script + +uninstall_unix() { + echo "Uninstalling backrest from /usr/local/bin/backrest" + sudo rm -f /usr/local/bin/backrest +} + +remove_systemd_service() { + if [ ! -d /etc/systemd/system ]; then + echo "Systemd not found. This script is only for systemd based systems." + exit 1 + fi + + echo "Removing systemd service at /etc/systemd/system/backrest.service" + sudo systemctl stop backrest + sudo systemctl disable backrest + sudo rm -f /etc/systemd/system/backrest.service + + echo "Reloading systemd daemon" + sudo systemctl daemon-reload +} + +remove_launchd_plist() { + echo "Removing launchd plist at /Library/LaunchAgents/com.backrest.plist" + + launchctl unload /Library/LaunchAgents/com.backrest.plist || true + sudo rm /Library/LaunchAgents/com.backrest.plist +} + +OS=$(uname -s) +if [ "$OS" = "Darwin" ]; then + echo "Uninstalling on Darwin" + uninstall_unix + remove_launchd_plist + + echo "Done -- run 'launchctl list | grep backrest' to check the service installation." +elif [ "$OS" = "Linux" ]; then + echo "Unnstalling on Linux" + uninstall_unix + remove_systemd_service + + echo "Done -- run 'systemctl status backrest' to check the status of the service." +else + echo "Unknown OS: $OS. This script only supports Darwin and Linux." + exit 1 +fi diff --git a/webui/.gitignore b/webui/.gitignore new file mode 100644 index 000000000..75f5a6dec --- /dev/null +++ b/webui/.gitignore @@ -0,0 +1,4 @@ +.parcel-cache +node_modules +dist +dist-windows diff --git a/webui/.proxyrc.json b/webui/.proxyrc.json new file mode 100644 index 000000000..24cf63ef6 --- /dev/null +++ b/webui/.proxyrc.json @@ -0,0 +1,10 @@ +{ + "/v1.Backrest": { + "target": "http://localhost:9898", + "secure": false + }, + "/v1.Authentication": { + "target": "http://localhost:9898", + "secure": false + } +} diff --git a/webui/assets/favicon.png b/webui/assets/favicon.png new file mode 100644 index 000000000..cc06b543b Binary files /dev/null and b/webui/assets/favicon.png differ diff --git a/webui/assets/favicon.svg b/webui/assets/favicon.svg new file mode 100644 index 000000000..c494fedd5 --- /dev/null +++ b/webui/assets/favicon.svg @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/webui/assets/logo-black.svg b/webui/assets/logo-black.svg new file mode 100644 index 000000000..d96dade48 --- /dev/null +++ b/webui/assets/logo-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webui/assets/logo.svg b/webui/assets/logo.svg new file mode 100644 index 000000000..709910464 --- /dev/null +++ b/webui/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webui/gen/ts/google/api/annotations_pb.ts b/webui/gen/ts/google/api/annotations_pb.ts new file mode 100644 index 000000000..ad1423628 --- /dev/null +++ b/webui/gen/ts/google/api/annotations_pb.ts @@ -0,0 +1,39 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file google/api/annotations.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenExtension, GenFile } from "@bufbuild/protobuf/codegenv1"; +import { extDesc, fileDesc } from "@bufbuild/protobuf/codegenv1"; +import type { HttpRule } from "./http_pb"; +import { file_google_api_http } from "./http_pb"; +import type { MethodOptions } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt"; + +/** + * Describes the file google/api/annotations.proto. + */ +export const file_google_api_annotations: GenFile = /*@__PURE__*/ + fileDesc("Chxnb29nbGUvYXBpL2Fubm90YXRpb25zLnByb3RvEgpnb29nbGUuYXBpOksKBGh0dHASHi5nb29nbGUucHJvdG9idWYuTWV0aG9kT3B0aW9ucxiwyrwiIAEoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSBGh0dHBCbgoOY29tLmdvb2dsZS5hcGlCEEFubm90YXRpb25zUHJvdG9QAVpBZ29vZ2xlLmdvbGFuZy5vcmcvZ2VucHJvdG8vZ29vZ2xlYXBpcy9hcGkvYW5ub3RhdGlvbnM7YW5ub3RhdGlvbnOiAgRHQVBJYgZwcm90bzM", [file_google_api_http, file_google_protobuf_descriptor]); + +/** + * See `HttpRule`. + * + * @generated from extension: google.api.HttpRule http = 72295728; + */ +export const http: GenExtension = /*@__PURE__*/ + extDesc(file_google_api_annotations, 0); + diff --git a/webui/gen/ts/google/api/http_pb.ts b/webui/gen/ts/google/api/http_pb.ts new file mode 100644 index 000000000..2fe4b7b3f --- /dev/null +++ b/webui/gen/ts/google/api/http_pb.ts @@ -0,0 +1,482 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file google/api/http.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file google/api/http.proto. + */ +export const file_google_api_http: GenFile = /*@__PURE__*/ + fileDesc("ChVnb29nbGUvYXBpL2h0dHAucHJvdG8SCmdvb2dsZS5hcGkiVAoESHR0cBIjCgVydWxlcxgBIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGUSJwofZnVsbHlfZGVjb2RlX3Jlc2VydmVkX2V4cGFuc2lvbhgCIAEoCCKBAgoISHR0cFJ1bGUSEAoIc2VsZWN0b3IYASABKAkSDQoDZ2V0GAIgASgJSAASDQoDcHV0GAMgASgJSAASDgoEcG9zdBgEIAEoCUgAEhAKBmRlbGV0ZRgFIAEoCUgAEg8KBXBhdGNoGAYgASgJSAASLwoGY3VzdG9tGAggASgLMh0uZ29vZ2xlLmFwaS5DdXN0b21IdHRwUGF0dGVybkgAEgwKBGJvZHkYByABKAkSFQoNcmVzcG9uc2VfYm9keRgMIAEoCRIxChNhZGRpdGlvbmFsX2JpbmRpbmdzGAsgAygLMhQuZ29vZ2xlLmFwaS5IdHRwUnVsZUIJCgdwYXR0ZXJuIi8KEUN1c3RvbUh0dHBQYXR0ZXJuEgwKBGtpbmQYASABKAkSDAoEcGF0aBgCIAEoCUJqCg5jb20uZ29vZ2xlLmFwaUIJSHR0cFByb3RvUAFaQWdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvYXBpL2Fubm90YXRpb25zO2Fubm90YXRpb25z+AEBogIER0FQSWIGcHJvdG8z"); + +/** + * Defines the HTTP configuration for an API service. It contains a list of + * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method + * to one or more HTTP REST API methods. + * + * @generated from message google.api.Http + */ +export type Http = Message<"google.api.Http"> & { + /** + * A list of HTTP configuration rules that apply to individual API methods. + * + * **NOTE:** All service configuration rules follow "last one wins" order. + * + * @generated from field: repeated google.api.HttpRule rules = 1; + */ + rules: HttpRule[]; + + /** + * When set to true, URL path parameters will be fully URI-decoded except in + * cases of single segment matches in reserved expansion, where "%2F" will be + * left encoded. + * + * The default behavior is to not decode RFC 6570 reserved characters in multi + * segment matches. + * + * @generated from field: bool fully_decode_reserved_expansion = 2; + */ + fullyDecodeReservedExpansion: boolean; +}; + +/** + * Describes the message google.api.Http. + * Use `create(HttpSchema)` to create a new message. + */ +export const HttpSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 0); + +/** + * # gRPC Transcoding + * + * gRPC Transcoding is a feature for mapping between a gRPC method and one or + * more HTTP REST endpoints. It allows developers to build a single API service + * that supports both gRPC APIs and REST APIs. Many systems, including [Google + * APIs](https://github.com/googleapis/googleapis), + * [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC + * Gateway](https://github.com/grpc-ecosystem/grpc-gateway), + * and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature + * and use it for large scale production services. + * + * `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies + * how different portions of the gRPC request message are mapped to the URL + * path, URL query parameters, and HTTP request body. It also controls how the + * gRPC response message is mapped to the HTTP response body. `HttpRule` is + * typically specified as an `google.api.http` annotation on the gRPC method. + * + * Each mapping specifies a URL path template and an HTTP method. The path + * template may refer to one or more fields in the gRPC request message, as long + * as each field is a non-repeated field with a primitive (non-message) type. + * The path template controls how fields of the request message are mapped to + * the URL path. + * + * Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/{name=messages/*}" + * }; + * } + * } + * message GetMessageRequest { + * string name = 1; // Mapped to URL path. + * } + * message Message { + * string text = 1; // The resource content. + * } + * + * This enables an HTTP REST to gRPC mapping as below: + * + * HTTP | gRPC + * -----|----- + * `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` + * + * Any fields in the request message which are not bound by the path template + * automatically become HTTP query parameters if there is no HTTP request body. + * For example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get:"/v1/messages/{message_id}" + * }; + * } + * } + * message GetMessageRequest { + * message SubMessage { + * string subfield = 1; + * } + * string message_id = 1; // Mapped to URL path. + * int64 revision = 2; // Mapped to URL query parameter `revision`. + * SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. + * } + * + * This enables a HTTP JSON to RPC mapping as below: + * + * HTTP | gRPC + * -----|----- + * `GET /v1/messages/123456?revision=2&sub.subfield=foo` | + * `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: + * "foo"))` + * + * Note that fields which are mapped to URL query parameters must have a + * primitive type or a repeated primitive type or a non-repeated message type. + * In the case of a repeated type, the parameter can be repeated in the URL + * as `...?param=A¶m=B`. In the case of a message type, each field of the + * message is mapped to a separate parameter, such as + * `...?foo.a=A&foo.b=B&foo.c=C`. + * + * For HTTP methods that allow a request body, the `body` field + * specifies the mapping. Consider a REST update method on the + * message resource collection: + * + * service Messaging { + * rpc UpdateMessage(UpdateMessageRequest) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "message" + * }; + * } + * } + * message UpdateMessageRequest { + * string message_id = 1; // mapped to the URL + * Message message = 2; // mapped to the body + * } + * + * The following HTTP JSON to RPC mapping is enabled, where the + * representation of the JSON in the request body is determined by + * protos JSON encoding: + * + * HTTP | gRPC + * -----|----- + * `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: + * "123456" message { text: "Hi!" })` + * + * The special name `*` can be used in the body mapping to define that + * every field not bound by the path template should be mapped to the + * request body. This enables the following alternative definition of + * the update method: + * + * service Messaging { + * rpc UpdateMessage(Message) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "*" + * }; + * } + * } + * message Message { + * string message_id = 1; + * string text = 2; + * } + * + * + * The following HTTP JSON to RPC mapping is enabled: + * + * HTTP | gRPC + * -----|----- + * `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: + * "123456" text: "Hi!")` + * + * Note that when using `*` in the body mapping, it is not possible to + * have HTTP parameters, as all fields not bound by the path end in + * the body. This makes this option more rarely used in practice when + * defining REST APIs. The common usage of `*` is in custom methods + * which don't use the URL at all for transferring data. + * + * It is possible to define multiple HTTP methods for one RPC by using + * the `additional_bindings` option. Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/messages/{message_id}" + * additional_bindings { + * get: "/v1/users/{user_id}/messages/{message_id}" + * } + * }; + * } + * } + * message GetMessageRequest { + * string message_id = 1; + * string user_id = 2; + * } + * + * This enables the following two alternative HTTP JSON to RPC mappings: + * + * HTTP | gRPC + * -----|----- + * `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` + * `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: + * "123456")` + * + * ## Rules for HTTP mapping + * + * 1. Leaf request fields (recursive expansion nested messages in the request + * message) are classified into three categories: + * - Fields referred by the path template. They are passed via the URL path. + * - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They + * are passed via the HTTP + * request body. + * - All other fields are passed via the URL query parameters, and the + * parameter name is the field path in the request message. A repeated + * field can be represented as multiple query parameters under the same + * name. + * 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL + * query parameter, all fields + * are passed via URL path and HTTP request body. + * 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP + * request body, all + * fields are passed via URL path and URL query parameters. + * + * ### Path template syntax + * + * Template = "/" Segments [ Verb ] ; + * Segments = Segment { "/" Segment } ; + * Segment = "*" | "**" | LITERAL | Variable ; + * Variable = "{" FieldPath [ "=" Segments ] "}" ; + * FieldPath = IDENT { "." IDENT } ; + * Verb = ":" LITERAL ; + * + * The syntax `*` matches a single URL path segment. The syntax `**` matches + * zero or more URL path segments, which must be the last part of the URL path + * except the `Verb`. + * + * The syntax `Variable` matches part of the URL path as specified by its + * template. A variable template must not contain other variables. If a variable + * matches a single path segment, its template may be omitted, e.g. `{var}` + * is equivalent to `{var=*}`. + * + * The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` + * contains any reserved character, such characters should be percent-encoded + * before the matching. + * + * If a variable contains exactly one path segment, such as `"{var}"` or + * `"{var=*}"`, when such a variable is expanded into a URL path on the client + * side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The + * server side does the reverse decoding. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{var}`. + * + * If a variable contains multiple path segments, such as `"{var=foo/*}"` + * or `"{var=**}"`, when such a variable is expanded into a URL path on the + * client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. + * The server side does the reverse decoding, except "%2F" and "%2f" are left + * unchanged. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{+var}`. + * + * ## Using gRPC API Service Configuration + * + * gRPC API Service Configuration (service config) is a configuration language + * for configuring a gRPC service to become a user-facing product. The + * service config is simply the YAML representation of the `google.api.Service` + * proto message. + * + * As an alternative to annotating your proto file, you can configure gRPC + * transcoding in your service config YAML files. You do this by specifying a + * `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same + * effect as the proto annotation. This can be particularly useful if you + * have a proto that is reused in multiple services. Note that any transcoding + * specified in the service config will override any matching transcoding + * configuration in the proto. + * + * Example: + * + * http: + * rules: + * # Selects a gRPC method and applies HttpRule to it. + * - selector: example.v1.Messaging.GetMessage + * get: /v1/messages/{message_id}/{sub.subfield} + * + * ## Special notes + * + * When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the + * proto to JSON conversion must follow the [proto3 + * specification](https://developers.google.com/protocol-buffers/docs/proto3#json). + * + * While the single segment variable follows the semantics of + * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String + * Expansion, the multi segment variable **does not** follow RFC 6570 Section + * 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion + * does not expand special characters like `?` and `#`, which would lead + * to invalid URLs. As the result, gRPC Transcoding uses a custom encoding + * for multi segment variables. + * + * The path variables **must not** refer to any repeated or mapped field, + * because client libraries are not capable of handling such variable expansion. + * + * The path variables **must not** capture the leading "/" character. The reason + * is that the most common use case "{var}" does not capture the leading "/" + * character. For consistency, all path variables must share the same behavior. + * + * Repeated message fields must not be mapped to URL query parameters, because + * no client library can support such complicated mapping. + * + * If an API needs to use a JSON array for request or response body, it can map + * the request or response body to a repeated field. However, some gRPC + * Transcoding implementations may not support this feature. + * + * @generated from message google.api.HttpRule + */ +export type HttpRule = Message<"google.api.HttpRule"> & { + /** + * Selects a method to which this rule applies. + * + * Refer to [selector][google.api.DocumentationRule.selector] for syntax + * details. + * + * @generated from field: string selector = 1; + */ + selector: string; + + /** + * Determines the URL pattern is matched by this rules. This pattern can be + * used with any of the {get|put|post|delete|patch} methods. A custom method + * can be defined using the 'custom' field. + * + * @generated from oneof google.api.HttpRule.pattern + */ + pattern: { + /** + * Maps to HTTP GET. Used for listing and getting information about + * resources. + * + * @generated from field: string get = 2; + */ + value: string; + case: "get"; + } | { + /** + * Maps to HTTP PUT. Used for replacing a resource. + * + * @generated from field: string put = 3; + */ + value: string; + case: "put"; + } | { + /** + * Maps to HTTP POST. Used for creating a resource or performing an action. + * + * @generated from field: string post = 4; + */ + value: string; + case: "post"; + } | { + /** + * Maps to HTTP DELETE. Used for deleting a resource. + * + * @generated from field: string delete = 5; + */ + value: string; + case: "delete"; + } | { + /** + * Maps to HTTP PATCH. Used for updating a resource. + * + * @generated from field: string patch = 6; + */ + value: string; + case: "patch"; + } | { + /** + * The custom pattern is used for specifying an HTTP method that is not + * included in the `pattern` field, such as HEAD, or "*" to leave the + * HTTP method unspecified for this rule. The wild-card rule is useful + * for services that provide content to Web (HTML) clients. + * + * @generated from field: google.api.CustomHttpPattern custom = 8; + */ + value: CustomHttpPattern; + case: "custom"; + } | { case: undefined; value?: undefined }; + + /** + * The name of the request field whose value is mapped to the HTTP request + * body, or `*` for mapping all request fields not captured by the path + * pattern to the HTTP body, or omitted for not having any HTTP request body. + * + * NOTE: the referred field must be present at the top-level of the request + * message type. + * + * @generated from field: string body = 7; + */ + body: string; + + /** + * Optional. The name of the response field whose value is mapped to the HTTP + * response body. When omitted, the entire response message will be used + * as the HTTP response body. + * + * NOTE: The referred field must be present at the top-level of the response + * message type. + * + * @generated from field: string response_body = 12; + */ + responseBody: string; + + /** + * Additional HTTP bindings for the selector. Nested bindings must + * not contain an `additional_bindings` field themselves (that is, + * the nesting may only be one level deep). + * + * @generated from field: repeated google.api.HttpRule additional_bindings = 11; + */ + additionalBindings: HttpRule[]; +}; + +/** + * Describes the message google.api.HttpRule. + * Use `create(HttpRuleSchema)` to create a new message. + */ +export const HttpRuleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 1); + +/** + * A custom pattern is used for defining custom HTTP verb. + * + * @generated from message google.api.CustomHttpPattern + */ +export type CustomHttpPattern = Message<"google.api.CustomHttpPattern"> & { + /** + * The name of this custom HTTP verb. + * + * @generated from field: string kind = 1; + */ + kind: string; + + /** + * The path matched by this custom verb. + * + * @generated from field: string path = 2; + */ + path: string; +}; + +/** + * Describes the message google.api.CustomHttpPattern. + * Use `create(CustomHttpPatternSchema)` to create a new message. + */ +export const CustomHttpPatternSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 2); + diff --git a/webui/gen/ts/types/value_pb.ts b/webui/gen/ts/types/value_pb.ts new file mode 100644 index 000000000..2187a785e --- /dev/null +++ b/webui/gen/ts/types/value_pb.ts @@ -0,0 +1,129 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file types/value.proto (package types, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file types/value.proto. + */ +export const file_types_value: GenFile = /*@__PURE__*/ + fileDesc("ChF0eXBlcy92YWx1ZS5wcm90bxIFdHlwZXMiGgoJQm9vbFZhbHVlEg0KBXZhbHVlGAEgASgIIhwKC1N0cmluZ1ZhbHVlEg0KBXZhbHVlGAEgASgJIhsKCkJ5dGVzVmFsdWUSDQoFdmFsdWUYASABKAwiHAoKU3RyaW5nTGlzdBIOCgZ2YWx1ZXMYASADKAkiGwoKSW50NjRWYWx1ZRINCgV2YWx1ZRgBIAEoAyIbCglJbnQ2NExpc3QSDgoGdmFsdWVzGAEgAygDIgcKBUVtcHR5Qi9aLWdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby90eXBlc2IGcHJvdG8z"); + +/** + * @generated from message types.BoolValue + */ +export type BoolValue = Message<"types.BoolValue"> & { + /** + * @generated from field: bool value = 1; + */ + value: boolean; +}; + +/** + * Describes the message types.BoolValue. + * Use `create(BoolValueSchema)` to create a new message. + */ +export const BoolValueSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 0); + +/** + * @generated from message types.StringValue + */ +export type StringValue = Message<"types.StringValue"> & { + /** + * @generated from field: string value = 1; + */ + value: string; +}; + +/** + * Describes the message types.StringValue. + * Use `create(StringValueSchema)` to create a new message. + */ +export const StringValueSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 1); + +/** + * @generated from message types.BytesValue + */ +export type BytesValue = Message<"types.BytesValue"> & { + /** + * @generated from field: bytes value = 1; + */ + value: Uint8Array; +}; + +/** + * Describes the message types.BytesValue. + * Use `create(BytesValueSchema)` to create a new message. + */ +export const BytesValueSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 2); + +/** + * @generated from message types.StringList + */ +export type StringList = Message<"types.StringList"> & { + /** + * @generated from field: repeated string values = 1; + */ + values: string[]; +}; + +/** + * Describes the message types.StringList. + * Use `create(StringListSchema)` to create a new message. + */ +export const StringListSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 3); + +/** + * @generated from message types.Int64Value + */ +export type Int64Value = Message<"types.Int64Value"> & { + /** + * @generated from field: int64 value = 1; + */ + value: bigint; +}; + +/** + * Describes the message types.Int64Value. + * Use `create(Int64ValueSchema)` to create a new message. + */ +export const Int64ValueSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 4); + +/** + * @generated from message types.Int64List + */ +export type Int64List = Message<"types.Int64List"> & { + /** + * @generated from field: repeated int64 values = 1; + */ + values: bigint[]; +}; + +/** + * Describes the message types.Int64List. + * Use `create(Int64ListSchema)` to create a new message. + */ +export const Int64ListSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 5); + +/** + * @generated from message types.Empty + */ +export type Empty = Message<"types.Empty"> & { +}; + +/** + * Describes the message types.Empty. + * Use `create(EmptySchema)` to create a new message. + */ +export const EmptySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_types_value, 6); + diff --git a/webui/gen/ts/v1/authentication_pb.ts b/webui/gen/ts/v1/authentication_pb.ts new file mode 100644 index 000000000..e26d3ddd9 --- /dev/null +++ b/webui/gen/ts/v1/authentication_pb.ts @@ -0,0 +1,83 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/authentication.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import { file_v1_config } from "./config_pb"; +import type { StringValueSchema } from "../types/value_pb"; +import { file_types_value } from "../types/value_pb"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import { file_google_api_annotations } from "../google/api/annotations_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/authentication.proto. + */ +export const file_v1_authentication: GenFile = /*@__PURE__*/ + fileDesc("Chd2MS9hdXRoZW50aWNhdGlvbi5wcm90bxICdjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkyegoOQXV0aGVudGljYXRpb24SLgoFTG9naW4SEC52MS5Mb2dpblJlcXVlc3QaES52MS5Mb2dpblJlc3BvbnNlIgASOAoMSGFzaFBhc3N3b3JkEhIudHlwZXMuU3RyaW5nVmFsdWUaEi50eXBlcy5TdHJpbmdWYWx1ZSIAQixaKmdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby92MWIGcHJvdG8z", [file_v1_config, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); + +/** + * @generated from message v1.LoginRequest + */ +export type LoginRequest = Message<"v1.LoginRequest"> & { + /** + * @generated from field: string username = 1; + */ + username: string; + + /** + * @generated from field: string password = 2; + */ + password: string; +}; + +/** + * Describes the message v1.LoginRequest. + * Use `create(LoginRequestSchema)` to create a new message. + */ +export const LoginRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_authentication, 0); + +/** + * @generated from message v1.LoginResponse + */ +export type LoginResponse = Message<"v1.LoginResponse"> & { + /** + * JWT token + * + * @generated from field: string token = 1; + */ + token: string; +}; + +/** + * Describes the message v1.LoginResponse. + * Use `create(LoginResponseSchema)` to create a new message. + */ +export const LoginResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_authentication, 1); + +/** + * @generated from service v1.Authentication + */ +export const Authentication: GenService<{ + /** + * @generated from rpc v1.Authentication.Login + */ + login: { + methodKind: "unary"; + input: typeof LoginRequestSchema; + output: typeof LoginResponseSchema; + }, + /** + * @generated from rpc v1.Authentication.HashPassword + */ + hashPassword: { + methodKind: "unary"; + input: typeof StringValueSchema; + output: typeof StringValueSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_v1_authentication, 0); + diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts new file mode 100644 index 000000000..627ab0b4e --- /dev/null +++ b/webui/gen/ts/v1/config_pb.ts @@ -0,0 +1,1199 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/config.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import type { PublicKey } from "./crypto_pb"; +import { file_v1_crypto } from "./crypto_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/config.proto. + */ +export const file_v1_config: GenFile = /*@__PURE__*/ + fileDesc("Cg92MS9jb25maWcucHJvdG8SAnYxImYKCUh1YkNvbmZpZxItCglpbnN0YW5jZXMYASADKAsyGi52MS5IdWJDb25maWcuSW5zdGFuY2VJbmZvGioKDEluc3RhbmNlSW5mbxIKCgJpZBgBIAEoCRIOCgZzZWNyZXQYAiABKAkirAEKBkNvbmZpZxINCgVtb2RubxgBIAEoBRIPCgd2ZXJzaW9uGAYgASgFEhAKCGluc3RhbmNlGAIgASgJEhcKBXJlcG9zGAMgAygLMggudjEuUmVwbxIXCgVwbGFucxgEIAMoCzIILnYxLlBsYW4SFgoEYXV0aBgFIAEoCzIILnYxLkF1dGgSJgoJbXVsdGlob3N0GAcgASgLMg0udjEuTXVsdGlob3N0UgRzeW5jItcBCglNdWx0aWhvc3QSJwoLa25vd25faG9zdHMYASADKAsyEi52MS5NdWx0aWhvc3QuUGVlchIuChJhdXRob3JpemVkX2NsaWVudHMYAiADKAsyEi52MS5NdWx0aWhvc3QuUGVlchpxCgRQZWVyEhMKC2luc3RhbmNlX2lkGAEgASgJEiEKCnB1YmxpY19rZXkYAyABKAsyDS52MS5QdWJsaWNLZXkSGwoTcHVibGljX2tleV92ZXJpZmllZBgEIAEoCBIUCgxpbnN0YW5jZV91cmwYAiABKAkizAIKBFJlcG8SCgoCaWQYASABKAkSCwoDdXJpGAIgASgJEgwKBGd1aWQYCyABKAkSEAoIcGFzc3dvcmQYAyABKAkSCwoDZW52GAQgAygJEg0KBWZsYWdzGAUgAygJEiUKDHBydW5lX3BvbGljeRgGIAEoCzIPLnYxLlBydW5lUG9saWN5EiUKDGNoZWNrX3BvbGljeRgJIAEoCzIPLnYxLkNoZWNrUG9saWN5EhcKBWhvb2tzGAcgAygLMggudjEuSG9vaxITCgthdXRvX3VubG9jaxgIIAEoCBIXCg9hdXRvX2luaXRpYWxpemUYDCABKAgSKQoOY29tbWFuZF9wcmVmaXgYCiABKAsyES52MS5Db21tYW5kUHJlZml4Ei8KGWFsbG93ZWRfcGVlcl9pbnN0YW5jZV9pZHMYZCADKAlSDGFsbG93ZWRQZWVycyKGAgoEUGxhbhIKCgJpZBgBIAEoCRIMCgRyZXBvGAIgASgJEg0KBXBhdGhzGAQgAygJEhAKCGV4Y2x1ZGVzGAUgAygJEhEKCWlleGNsdWRlcxgJIAMoCRIeCghzY2hlZHVsZRgMIAEoCzIMLnYxLlNjaGVkdWxlEiYKCXJldGVudGlvbhgHIAEoCzITLnYxLlJldGVudGlvblBvbGljeRIXCgVob29rcxgIIAMoCzIILnYxLkhvb2sSIgoMYmFja3VwX2ZsYWdzGAogAygJUgxiYWNrdXBfZmxhZ3MSGQoRc2tpcF9pZl91bmNoYW5nZWQYDSABKAhKBAgDEARKBAgGEAdKBAgLEAwiigIKDUNvbW1hbmRQcmVmaXgSLgoHaW9fbmljZRgBIAEoDjIdLnYxLkNvbW1hbmRQcmVmaXguSU9OaWNlTGV2ZWwSMAoIY3B1X25pY2UYAiABKA4yHi52MS5Db21tYW5kUHJlZml4LkNQVU5pY2VMZXZlbCJbCgtJT05pY2VMZXZlbBIOCgpJT19ERUZBVUxUEAASFgoSSU9fQkVTVF9FRkZPUlRfTE9XEAESFwoTSU9fQkVTVF9FRkZPUlRfSElHSBACEgsKB0lPX0lETEUQAyI6CgxDUFVOaWNlTGV2ZWwSDwoLQ1BVX0RFRkFVTFQQABIMCghDUFVfSElHSBABEgsKB0NQVV9MT1cQAiKCAgoPUmV0ZW50aW9uUG9saWN5EhwKEnBvbGljeV9rZWVwX2xhc3RfbhgKIAEoBUgAEkYKFHBvbGljeV90aW1lX2J1Y2tldGVkGAsgASgLMiYudjEuUmV0ZW50aW9uUG9saWN5LlRpbWVCdWNrZXRlZENvdW50c0gAEhkKD3BvbGljeV9rZWVwX2FsbBgMIAEoCEgAGmQKElRpbWVCdWNrZXRlZENvdW50cxIOCgZob3VybHkYASABKAUSDQoFZGFpbHkYAiABKAUSDgoGd2Vla2x5GAMgASgFEg8KB21vbnRobHkYBCABKAUSDgoGeWVhcmx5GAUgASgFQggKBnBvbGljeSJjCgtQcnVuZVBvbGljeRIeCghzY2hlZHVsZRgCIAEoCzIMLnYxLlNjaGVkdWxlEhgKEG1heF91bnVzZWRfYnl0ZXMYAyABKAMSGgoSbWF4X3VudXNlZF9wZXJjZW50GAQgASgBInMKC0NoZWNrUG9saWN5Eh4KCHNjaGVkdWxlGAEgASgLMgwudjEuU2NoZWR1bGUSGAoOc3RydWN0dXJlX29ubHkYZCABKAhIABIiChhyZWFkX2RhdGFfc3Vic2V0X3BlcmNlbnQYZSABKAFIAEIGCgRtb2RlIusBCghTY2hlZHVsZRISCghkaXNhYmxlZBgBIAEoCEgAEg4KBGNyb24YAiABKAlIABIaChBtYXhGcmVxdWVuY3lEYXlzGAMgASgFSAASGwoRbWF4RnJlcXVlbmN5SG91cnMYBCABKAVIABIhCgVjbG9jaxgFIAEoDjISLnYxLlNjaGVkdWxlLkNsb2NrIlMKBUNsb2NrEhEKDUNMT0NLX0RFRkFVTFQQABIPCgtDTE9DS19MT0NBTBABEg0KCUNMT0NLX1VUQxACEhcKE0NMT0NLX0xBU1RfUlVOX1RJTUUQA0IKCghzY2hlZHVsZSKQDAoESG9vaxImCgpjb25kaXRpb25zGAEgAygOMhIudjEuSG9vay5Db25kaXRpb24SIgoIb25fZXJyb3IYAiABKA4yEC52MS5Ib29rLk9uRXJyb3ISKgoOYWN0aW9uX2NvbW1hbmQYZCABKAsyEC52MS5Ib29rLkNvbW1hbmRIABIqCg5hY3Rpb25fd2ViaG9vaxhlIAEoCzIQLnYxLkhvb2suV2ViaG9va0gAEioKDmFjdGlvbl9kaXNjb3JkGGYgASgLMhAudjEuSG9vay5EaXNjb3JkSAASKAoNYWN0aW9uX2dvdGlmeRhnIAEoCzIPLnYxLkhvb2suR290aWZ5SAASJgoMYWN0aW9uX3NsYWNrGGggASgLMg4udjEuSG9vay5TbGFja0gAEiwKD2FjdGlvbl9zaG91dHJychhpIAEoCzIRLnYxLkhvb2suU2hvdXRycnJIABI0ChNhY3Rpb25faGVhbHRoY2hlY2tzGGogASgLMhUudjEuSG9vay5IZWFsdGhjaGVja3NIABoaCgdDb21tYW5kEg8KB2NvbW1hbmQYASABKAkagwEKB1dlYmhvb2sSEwoLd2ViaG9va191cmwYASABKAkSJwoGbWV0aG9kGAIgASgOMhcudjEuSG9vay5XZWJob29rLk1ldGhvZBIQCgh0ZW1wbGF0ZRhkIAEoCSIoCgZNZXRob2QSCwoHVU5LTk9XThAAEgcKA0dFVBABEggKBFBPU1QQAhowCgdEaXNjb3JkEhMKC3dlYmhvb2tfdXJsGAEgASgJEhAKCHRlbXBsYXRlGAIgASgJGmUKBkdvdGlmeRIQCghiYXNlX3VybBgBIAEoCRINCgV0b2tlbhgDIAEoCRIQCgh0ZW1wbGF0ZRhkIAEoCRIWCg50aXRsZV90ZW1wbGF0ZRhlIAEoCRIQCghwcmlvcml0eRhmIAEoBRouCgVTbGFjaxITCgt3ZWJob29rX3VybBgBIAEoCRIQCgh0ZW1wbGF0ZRgCIAEoCRoyCghTaG91dHJychIUCgxzaG91dHJycl91cmwYASABKAkSEAoIdGVtcGxhdGUYAiABKAkaNQoMSGVhbHRoY2hlY2tzEhMKC3dlYmhvb2tfdXJsGAEgASgJEhAKCHRlbXBsYXRlGAIgASgJIvUDCglDb25kaXRpb24SFQoRQ09ORElUSU9OX1VOS05PV04QABIXChNDT05ESVRJT05fQU5ZX0VSUk9SEAESHAoYQ09ORElUSU9OX1NOQVBTSE9UX1NUQVJUEAISGgoWQ09ORElUSU9OX1NOQVBTSE9UX0VORBADEhwKGENPTkRJVElPTl9TTkFQU0hPVF9FUlJPUhAEEh4KGkNPTkRJVElPTl9TTkFQU0hPVF9XQVJOSU5HEAUSHgoaQ09ORElUSU9OX1NOQVBTSE9UX1NVQ0NFU1MQBhIeChpDT05ESVRJT05fU05BUFNIT1RfU0tJUFBFRBAHEhkKFUNPTkRJVElPTl9QUlVORV9TVEFSVBBkEhkKFUNPTkRJVElPTl9QUlVORV9FUlJPUhBlEhsKF0NPTkRJVElPTl9QUlVORV9TVUNDRVNTEGYSGgoVQ09ORElUSU9OX0NIRUNLX1NUQVJUEMgBEhoKFUNPTkRJVElPTl9DSEVDS19FUlJPUhDJARIcChdDT05ESVRJT05fQ0hFQ0tfU1VDQ0VTUxDKARIbChZDT05ESVRJT05fRk9SR0VUX1NUQVJUEKwCEhsKFkNPTkRJVElPTl9GT1JHRVRfRVJST1IQrQISHQoYQ09ORElUSU9OX0ZPUkdFVF9TVUNDRVNTEK4CIqkBCgdPbkVycm9yEhMKD09OX0VSUk9SX0lHTk9SRRAAEhMKD09OX0VSUk9SX0NBTkNFTBABEhIKDk9OX0VSUk9SX0ZBVEFMEAISGgoWT05fRVJST1JfUkVUUllfMU1JTlVURRBkEhwKGE9OX0VSUk9SX1JFVFJZXzEwTUlOVVRFUxBlEiYKIk9OX0VSUk9SX1JFVFJZX0VYUE9ORU5USUFMX0JBQ0tPRkYQZ0IICgZhY3Rpb24iMQoEQXV0aBIQCghkaXNhYmxlZBgBIAEoCBIXCgV1c2VycxgCIAMoCzIILnYxLlVzZXIiOwoEVXNlchIMCgRuYW1lGAEgASgJEhkKD3Bhc3N3b3JkX2JjcnlwdBgCIAEoCUgAQgoKCHBhc3N3b3JkQixaKmdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby92MWIGcHJvdG8z", [file_google_protobuf_empty, file_v1_crypto]); + +/** + * @generated from message v1.HubConfig + */ +export type HubConfig = Message<"v1.HubConfig"> & { + /** + * @generated from field: repeated v1.HubConfig.InstanceInfo instances = 1; + */ + instances: HubConfig_InstanceInfo[]; +}; + +/** + * Describes the message v1.HubConfig. + * Use `create(HubConfigSchema)` to create a new message. + */ +export const HubConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 0); + +/** + * @generated from message v1.HubConfig.InstanceInfo + */ +export type HubConfig_InstanceInfo = Message<"v1.HubConfig.InstanceInfo"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * secret used to authenticate with the hub. + * + * @generated from field: string secret = 2; + */ + secret: string; +}; + +/** + * Describes the message v1.HubConfig.InstanceInfo. + * Use `create(HubConfig_InstanceInfoSchema)` to create a new message. + */ +export const HubConfig_InstanceInfoSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 0, 0); + +/** + * Config is the top level config object for restic UI. + * + * @generated from message v1.Config + */ +export type Config = Message<"v1.Config"> & { + /** + * modification number, used for read-modify-write consistency in the UI. Incremented on every write. + * + * @generated from field: int32 modno = 1; + */ + modno: number; + + /** + * version of the config file format. Used to determine when to run migrations. + * + * @generated from field: int32 version = 6; + */ + version: number; + + /** + * The instance name for the Backrest installation. + * This identifies backups created by this instance and is displayed in the UI. + * + * @generated from field: string instance = 2; + */ + instance: string; + + /** + * @generated from field: repeated v1.Repo repos = 3; + */ + repos: Repo[]; + + /** + * @generated from field: repeated v1.Plan plans = 4; + */ + plans: Plan[]; + + /** + * @generated from field: v1.Auth auth = 5; + */ + auth?: Auth; + + /** + * @generated from field: v1.Multihost multihost = 7 [json_name = "sync"]; + */ + multihost?: Multihost; +}; + +/** + * Describes the message v1.Config. + * Use `create(ConfigSchema)` to create a new message. + */ +export const ConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 1); + +/** + * @generated from message v1.Multihost + */ +export type Multihost = Message<"v1.Multihost"> & { + /** + * @generated from field: repeated v1.Multihost.Peer known_hosts = 1; + */ + knownHosts: Multihost_Peer[]; + + /** + * @generated from field: repeated v1.Multihost.Peer authorized_clients = 2; + */ + authorizedClients: Multihost_Peer[]; +}; + +/** + * Describes the message v1.Multihost. + * Use `create(MultihostSchema)` to create a new message. + */ +export const MultihostSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 2); + +/** + * @generated from message v1.Multihost.Peer + */ +export type Multihost_Peer = Message<"v1.Multihost.Peer"> & { + /** + * instance ID of the peer. + * + * @generated from field: string instance_id = 1; + */ + instanceId: string; + + /** + * public key of the peer. If changed, the peer must re-verify the public key. + * + * @generated from field: v1.PublicKey public_key = 3; + */ + publicKey?: PublicKey; + + /** + * whether the public key is verified. This must be set for a host to authenticate a client. Clients implicitly validate the first key they see on initial connection. + * + * @generated from field: bool public_key_verified = 4; + */ + publicKeyVerified: boolean; + + /** + * Known host only fields + * + * instance URL, required for a known host. Otherwise meaningless. + * + * @generated from field: string instance_url = 2; + */ + instanceUrl: string; +}; + +/** + * Describes the message v1.Multihost.Peer. + * Use `create(Multihost_PeerSchema)` to create a new message. + */ +export const Multihost_PeerSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 2, 0); + +/** + * @generated from message v1.Repo + */ +export type Repo = Message<"v1.Repo"> & { + /** + * unique but human readable ID for this repo. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * URI of the repo. + * + * @generated from field: string uri = 2; + */ + uri: string; + + /** + * a globally unique ID for this repo. Should be derived as the 'id' field in `restic cat config --json`. + * + * @generated from field: string guid = 11; + */ + guid: string; + + /** + * plaintext password + * + * @generated from field: string password = 3; + */ + password: string; + + /** + * extra environment variables to set for restic. + * + * @generated from field: repeated string env = 4; + */ + env: string[]; + + /** + * extra flags set on the restic command. + * + * @generated from field: repeated string flags = 5; + */ + flags: string[]; + + /** + * policy for when to run prune. + * + * @generated from field: v1.PrunePolicy prune_policy = 6; + */ + prunePolicy?: PrunePolicy; + + /** + * policy for when to run check. + * + * @generated from field: v1.CheckPolicy check_policy = 9; + */ + checkPolicy?: CheckPolicy; + + /** + * hooks to run on events for this repo. + * + * @generated from field: repeated v1.Hook hooks = 7; + */ + hooks: Hook[]; + + /** + * automatically unlock the repo when needed. + * + * @generated from field: bool auto_unlock = 8; + */ + autoUnlock: boolean; + + /** + * whether the repo should be auto-initialized if not found. + * + * @generated from field: bool auto_initialize = 12; + */ + autoInitialize: boolean; + + /** + * modifiers for the restic commands + * + * @generated from field: v1.CommandPrefix command_prefix = 10; + */ + commandPrefix?: CommandPrefix; + + /** + * list of peer instance IDs allowed to access this repo. + * + * @generated from field: repeated string allowed_peer_instance_ids = 100 [json_name = "allowedPeers"]; + */ + allowedPeerInstanceIds: string[]; +}; + +/** + * Describes the message v1.Repo. + * Use `create(RepoSchema)` to create a new message. + */ +export const RepoSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 3); + +/** + * @generated from message v1.Plan + */ +export type Plan = Message<"v1.Plan"> & { + /** + * unique but human readable ID for this plan. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * ID of the repo to use. + * + * @generated from field: string repo = 2; + */ + repo: string; + + /** + * paths to include in the backup. + * + * @generated from field: repeated string paths = 4; + */ + paths: string[]; + + /** + * glob patterns to exclude. + * + * @generated from field: repeated string excludes = 5; + */ + excludes: string[]; + + /** + * case insensitive glob patterns to exclude. + * + * @generated from field: repeated string iexcludes = 9; + */ + iexcludes: string[]; + + /** + * schedule for the backup. + * + * @generated from field: v1.Schedule schedule = 12; + */ + schedule?: Schedule; + + /** + * retention policy for snapshots. + * + * @generated from field: v1.RetentionPolicy retention = 7; + */ + retention?: RetentionPolicy; + + /** + * hooks to run on events for this plan. + * + * @generated from field: repeated v1.Hook hooks = 8; + */ + hooks: Hook[]; + + /** + * extra flags to set when running a backup command. + * + * @generated from field: repeated string backup_flags = 10 [json_name = "backup_flags"]; + */ + backupFlags: string[]; + + /** + * skip the backup if no changes are detected. + * + * @generated from field: bool skip_if_unchanged = 13; + */ + skipIfUnchanged: boolean; +}; + +/** + * Describes the message v1.Plan. + * Use `create(PlanSchema)` to create a new message. + */ +export const PlanSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 4); + +/** + * @generated from message v1.CommandPrefix + */ +export type CommandPrefix = Message<"v1.CommandPrefix"> & { + /** + * ionice level to set. + * + * @generated from field: v1.CommandPrefix.IONiceLevel io_nice = 1; + */ + ioNice: CommandPrefix_IONiceLevel; + + /** + * nice level to set. + * + * @generated from field: v1.CommandPrefix.CPUNiceLevel cpu_nice = 2; + */ + cpuNice: CommandPrefix_CPUNiceLevel; +}; + +/** + * Describes the message v1.CommandPrefix. + * Use `create(CommandPrefixSchema)` to create a new message. + */ +export const CommandPrefixSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 5); + +/** + * @generated from enum v1.CommandPrefix.IONiceLevel + */ +export enum CommandPrefix_IONiceLevel { + /** + * @generated from enum value: IO_DEFAULT = 0; + */ + IO_DEFAULT = 0, + + /** + * @generated from enum value: IO_BEST_EFFORT_LOW = 1; + */ + IO_BEST_EFFORT_LOW = 1, + + /** + * @generated from enum value: IO_BEST_EFFORT_HIGH = 2; + */ + IO_BEST_EFFORT_HIGH = 2, + + /** + * @generated from enum value: IO_IDLE = 3; + */ + IO_IDLE = 3, +} + +/** + * Describes the enum v1.CommandPrefix.IONiceLevel. + */ +export const CommandPrefix_IONiceLevelSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 5, 0); + +/** + * @generated from enum v1.CommandPrefix.CPUNiceLevel + */ +export enum CommandPrefix_CPUNiceLevel { + /** + * @generated from enum value: CPU_DEFAULT = 0; + */ + CPU_DEFAULT = 0, + + /** + * @generated from enum value: CPU_HIGH = 1; + */ + CPU_HIGH = 1, + + /** + * @generated from enum value: CPU_LOW = 2; + */ + CPU_LOW = 2, +} + +/** + * Describes the enum v1.CommandPrefix.CPUNiceLevel. + */ +export const CommandPrefix_CPUNiceLevelSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 5, 1); + +/** + * @generated from message v1.RetentionPolicy + */ +export type RetentionPolicy = Message<"v1.RetentionPolicy"> & { + /** + * @generated from oneof v1.RetentionPolicy.policy + */ + policy: { + /** + * @generated from field: int32 policy_keep_last_n = 10; + */ + value: number; + case: "policyKeepLastN"; + } | { + /** + * @generated from field: v1.RetentionPolicy.TimeBucketedCounts policy_time_bucketed = 11; + */ + value: RetentionPolicy_TimeBucketedCounts; + case: "policyTimeBucketed"; + } | { + /** + * @generated from field: bool policy_keep_all = 12; + */ + value: boolean; + case: "policyKeepAll"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.RetentionPolicy. + * Use `create(RetentionPolicySchema)` to create a new message. + */ +export const RetentionPolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 6); + +/** + * @generated from message v1.RetentionPolicy.TimeBucketedCounts + */ +export type RetentionPolicy_TimeBucketedCounts = Message<"v1.RetentionPolicy.TimeBucketedCounts"> & { + /** + * keep the last n hourly snapshots. + * + * @generated from field: int32 hourly = 1; + */ + hourly: number; + + /** + * keep the last n daily snapshots. + * + * @generated from field: int32 daily = 2; + */ + daily: number; + + /** + * keep the last n weekly snapshots. + * + * @generated from field: int32 weekly = 3; + */ + weekly: number; + + /** + * keep the last n monthly snapshots. + * + * @generated from field: int32 monthly = 4; + */ + monthly: number; + + /** + * keep the last n yearly snapshots. + * + * @generated from field: int32 yearly = 5; + */ + yearly: number; +}; + +/** + * Describes the message v1.RetentionPolicy.TimeBucketedCounts. + * Use `create(RetentionPolicy_TimeBucketedCountsSchema)` to create a new message. + */ +export const RetentionPolicy_TimeBucketedCountsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 6, 0); + +/** + * @generated from message v1.PrunePolicy + */ +export type PrunePolicy = Message<"v1.PrunePolicy"> & { + /** + * @generated from field: v1.Schedule schedule = 2; + */ + schedule?: Schedule; + + /** + * max unused bytes before running prune. + * + * @generated from field: int64 max_unused_bytes = 3; + */ + maxUnusedBytes: bigint; + + /** + * max unused percent before running prune. + * + * @generated from field: double max_unused_percent = 4; + */ + maxUnusedPercent: number; +}; + +/** + * Describes the message v1.PrunePolicy. + * Use `create(PrunePolicySchema)` to create a new message. + */ +export const PrunePolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 7); + +/** + * @generated from message v1.CheckPolicy + */ +export type CheckPolicy = Message<"v1.CheckPolicy"> & { + /** + * @generated from field: v1.Schedule schedule = 1; + */ + schedule?: Schedule; + + /** + * @generated from oneof v1.CheckPolicy.mode + */ + mode: { + /** + * only check the structure of the repo. No pack data is read. + * + * @generated from field: bool structure_only = 100; + */ + value: boolean; + case: "structureOnly"; + } | { + /** + * check a percentage of pack data. + * + * @generated from field: double read_data_subset_percent = 101; + */ + value: number; + case: "readDataSubsetPercent"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.CheckPolicy. + * Use `create(CheckPolicySchema)` to create a new message. + */ +export const CheckPolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 8); + +/** + * @generated from message v1.Schedule + */ +export type Schedule = Message<"v1.Schedule"> & { + /** + * @generated from oneof v1.Schedule.schedule + */ + schedule: { + /** + * disable the schedule. + * + * @generated from field: bool disabled = 1; + */ + value: boolean; + case: "disabled"; + } | { + /** + * cron expression describing the schedule. + * + * @generated from field: string cron = 2; + */ + value: string; + case: "cron"; + } | { + /** + * max frequency of runs in days. + * + * @generated from field: int32 maxFrequencyDays = 3; + */ + value: number; + case: "maxFrequencyDays"; + } | { + /** + * max frequency of runs in hours. + * + * @generated from field: int32 maxFrequencyHours = 4; + */ + value: number; + case: "maxFrequencyHours"; + } | { case: undefined; value?: undefined }; + + /** + * clock to use for scheduling. + * + * @generated from field: v1.Schedule.Clock clock = 5; + */ + clock: Schedule_Clock; +}; + +/** + * Describes the message v1.Schedule. + * Use `create(ScheduleSchema)` to create a new message. + */ +export const ScheduleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 9); + +/** + * @generated from enum v1.Schedule.Clock + */ +export enum Schedule_Clock { + /** + * same as CLOCK_LOCAL + * + * @generated from enum value: CLOCK_DEFAULT = 0; + */ + DEFAULT = 0, + + /** + * @generated from enum value: CLOCK_LOCAL = 1; + */ + LOCAL = 1, + + /** + * @generated from enum value: CLOCK_UTC = 2; + */ + UTC = 2, + + /** + * @generated from enum value: CLOCK_LAST_RUN_TIME = 3; + */ + LAST_RUN_TIME = 3, +} + +/** + * Describes the enum v1.Schedule.Clock. + */ +export const Schedule_ClockSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 9, 0); + +/** + * @generated from message v1.Hook + */ +export type Hook = Message<"v1.Hook"> & { + /** + * @generated from field: repeated v1.Hook.Condition conditions = 1; + */ + conditions: Hook_Condition[]; + + /** + * @generated from field: v1.Hook.OnError on_error = 2; + */ + onError: Hook_OnError; + + /** + * @generated from oneof v1.Hook.action + */ + action: { + /** + * @generated from field: v1.Hook.Command action_command = 100; + */ + value: Hook_Command; + case: "actionCommand"; + } | { + /** + * @generated from field: v1.Hook.Webhook action_webhook = 101; + */ + value: Hook_Webhook; + case: "actionWebhook"; + } | { + /** + * @generated from field: v1.Hook.Discord action_discord = 102; + */ + value: Hook_Discord; + case: "actionDiscord"; + } | { + /** + * @generated from field: v1.Hook.Gotify action_gotify = 103; + */ + value: Hook_Gotify; + case: "actionGotify"; + } | { + /** + * @generated from field: v1.Hook.Slack action_slack = 104; + */ + value: Hook_Slack; + case: "actionSlack"; + } | { + /** + * @generated from field: v1.Hook.Shoutrrr action_shoutrrr = 105; + */ + value: Hook_Shoutrrr; + case: "actionShoutrrr"; + } | { + /** + * @generated from field: v1.Hook.Healthchecks action_healthchecks = 106; + */ + value: Hook_Healthchecks; + case: "actionHealthchecks"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.Hook. + * Use `create(HookSchema)` to create a new message. + */ +export const HookSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10); + +/** + * @generated from message v1.Hook.Command + */ +export type Hook_Command = Message<"v1.Hook.Command"> & { + /** + * @generated from field: string command = 1; + */ + command: string; +}; + +/** + * Describes the message v1.Hook.Command. + * Use `create(Hook_CommandSchema)` to create a new message. + */ +export const Hook_CommandSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 0); + +/** + * @generated from message v1.Hook.Webhook + */ +export type Hook_Webhook = Message<"v1.Hook.Webhook"> & { + /** + * @generated from field: string webhook_url = 1; + */ + webhookUrl: string; + + /** + * @generated from field: v1.Hook.Webhook.Method method = 2; + */ + method: Hook_Webhook_Method; + + /** + * @generated from field: string template = 100; + */ + template: string; +}; + +/** + * Describes the message v1.Hook.Webhook. + * Use `create(Hook_WebhookSchema)` to create a new message. + */ +export const Hook_WebhookSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 1); + +/** + * @generated from enum v1.Hook.Webhook.Method + */ +export enum Hook_Webhook_Method { + /** + * @generated from enum value: UNKNOWN = 0; + */ + UNKNOWN = 0, + + /** + * @generated from enum value: GET = 1; + */ + GET = 1, + + /** + * @generated from enum value: POST = 2; + */ + POST = 2, +} + +/** + * Describes the enum v1.Hook.Webhook.Method. + */ +export const Hook_Webhook_MethodSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 10, 1, 0); + +/** + * @generated from message v1.Hook.Discord + */ +export type Hook_Discord = Message<"v1.Hook.Discord"> & { + /** + * @generated from field: string webhook_url = 1; + */ + webhookUrl: string; + + /** + * template for the webhook payload. + * + * @generated from field: string template = 2; + */ + template: string; +}; + +/** + * Describes the message v1.Hook.Discord. + * Use `create(Hook_DiscordSchema)` to create a new message. + */ +export const Hook_DiscordSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 2); + +/** + * @generated from message v1.Hook.Gotify + */ +export type Hook_Gotify = Message<"v1.Hook.Gotify"> & { + /** + * @generated from field: string base_url = 1; + */ + baseUrl: string; + + /** + * @generated from field: string token = 3; + */ + token: string; + + /** + * template for the webhook payload. + * + * @generated from field: string template = 100; + */ + template: string; + + /** + * template for the webhook title. + * + * @generated from field: string title_template = 101; + */ + titleTemplate: string; + + /** + * priority level for the notification (1-10) + * + * @generated from field: int32 priority = 102; + */ + priority: number; +}; + +/** + * Describes the message v1.Hook.Gotify. + * Use `create(Hook_GotifySchema)` to create a new message. + */ +export const Hook_GotifySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 3); + +/** + * @generated from message v1.Hook.Slack + */ +export type Hook_Slack = Message<"v1.Hook.Slack"> & { + /** + * @generated from field: string webhook_url = 1; + */ + webhookUrl: string; + + /** + * template for the webhook payload. + * + * @generated from field: string template = 2; + */ + template: string; +}; + +/** + * Describes the message v1.Hook.Slack. + * Use `create(Hook_SlackSchema)` to create a new message. + */ +export const Hook_SlackSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 4); + +/** + * @generated from message v1.Hook.Shoutrrr + */ +export type Hook_Shoutrrr = Message<"v1.Hook.Shoutrrr"> & { + /** + * @generated from field: string shoutrrr_url = 1; + */ + shoutrrrUrl: string; + + /** + * @generated from field: string template = 2; + */ + template: string; +}; + +/** + * Describes the message v1.Hook.Shoutrrr. + * Use `create(Hook_ShoutrrrSchema)` to create a new message. + */ +export const Hook_ShoutrrrSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 5); + +/** + * @generated from message v1.Hook.Healthchecks + */ +export type Hook_Healthchecks = Message<"v1.Hook.Healthchecks"> & { + /** + * @generated from field: string webhook_url = 1; + */ + webhookUrl: string; + + /** + * @generated from field: string template = 2; + */ + template: string; +}; + +/** + * Describes the message v1.Hook.Healthchecks. + * Use `create(Hook_HealthchecksSchema)` to create a new message. + */ +export const Hook_HealthchecksSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 10, 6); + +/** + * @generated from enum v1.Hook.Condition + */ +export enum Hook_Condition { + /** + * @generated from enum value: CONDITION_UNKNOWN = 0; + */ + UNKNOWN = 0, + + /** + * error running any operation. + * + * @generated from enum value: CONDITION_ANY_ERROR = 1; + */ + ANY_ERROR = 1, + + /** + * backup started. + * + * @generated from enum value: CONDITION_SNAPSHOT_START = 2; + */ + SNAPSHOT_START = 2, + + /** + * backup completed (success or fail). + * + * @generated from enum value: CONDITION_SNAPSHOT_END = 3; + */ + SNAPSHOT_END = 3, + + /** + * snapshot failed. + * + * @generated from enum value: CONDITION_SNAPSHOT_ERROR = 4; + */ + SNAPSHOT_ERROR = 4, + + /** + * snapshot completed with warnings. + * + * @generated from enum value: CONDITION_SNAPSHOT_WARNING = 5; + */ + SNAPSHOT_WARNING = 5, + + /** + * snapshot succeeded. + * + * @generated from enum value: CONDITION_SNAPSHOT_SUCCESS = 6; + */ + SNAPSHOT_SUCCESS = 6, + + /** + * snapshot was skipped e.g. due to no changes. + * + * @generated from enum value: CONDITION_SNAPSHOT_SKIPPED = 7; + */ + SNAPSHOT_SKIPPED = 7, + + /** + * prune conditions + * + * prune started. + * + * @generated from enum value: CONDITION_PRUNE_START = 100; + */ + PRUNE_START = 100, + + /** + * prune failed. + * + * @generated from enum value: CONDITION_PRUNE_ERROR = 101; + */ + PRUNE_ERROR = 101, + + /** + * prune succeeded. + * + * @generated from enum value: CONDITION_PRUNE_SUCCESS = 102; + */ + PRUNE_SUCCESS = 102, + + /** + * check conditions + * + * check started. + * + * @generated from enum value: CONDITION_CHECK_START = 200; + */ + CHECK_START = 200, + + /** + * check failed. + * + * @generated from enum value: CONDITION_CHECK_ERROR = 201; + */ + CHECK_ERROR = 201, + + /** + * check succeeded. + * + * @generated from enum value: CONDITION_CHECK_SUCCESS = 202; + */ + CHECK_SUCCESS = 202, + + /** + * forget conditions + * + * forget started. + * + * @generated from enum value: CONDITION_FORGET_START = 300; + */ + FORGET_START = 300, + + /** + * forget failed. + * + * @generated from enum value: CONDITION_FORGET_ERROR = 301; + */ + FORGET_ERROR = 301, + + /** + * forget succeeded. + * + * @generated from enum value: CONDITION_FORGET_SUCCESS = 302; + */ + FORGET_SUCCESS = 302, +} + +/** + * Describes the enum v1.Hook.Condition. + */ +export const Hook_ConditionSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 10, 0); + +/** + * @generated from enum v1.Hook.OnError + */ +export enum Hook_OnError { + /** + * @generated from enum value: ON_ERROR_IGNORE = 0; + */ + IGNORE = 0, + + /** + * cancels the operation and skips subsequent hooks + * + * @generated from enum value: ON_ERROR_CANCEL = 1; + */ + CANCEL = 1, + + /** + * fails the operation and subsequent hooks. + * + * @generated from enum value: ON_ERROR_FATAL = 2; + */ + FATAL = 2, + + /** + * retry the operation every minute + * + * @generated from enum value: ON_ERROR_RETRY_1MINUTE = 100; + */ + RETRY_1MINUTE = 100, + + /** + * retry the operation every 10 minutes + * + * @generated from enum value: ON_ERROR_RETRY_10MINUTES = 101; + */ + RETRY_10MINUTES = 101, + + /** + * retry the operation with exponential backoff up to 1h max. + * + * @generated from enum value: ON_ERROR_RETRY_EXPONENTIAL_BACKOFF = 103; + */ + RETRY_EXPONENTIAL_BACKOFF = 103, +} + +/** + * Describes the enum v1.Hook.OnError. + */ +export const Hook_OnErrorSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_config, 10, 1); + +/** + * @generated from message v1.Auth + */ +export type Auth = Message<"v1.Auth"> & { + /** + * disable authentication. + * + * @generated from field: bool disabled = 1; + */ + disabled: boolean; + + /** + * users to allow access to the UI. + * + * @generated from field: repeated v1.User users = 2; + */ + users: User[]; +}; + +/** + * Describes the message v1.Auth. + * Use `create(AuthSchema)` to create a new message. + */ +export const AuthSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 11); + +/** + * @generated from message v1.User + */ +export type User = Message<"v1.User"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from oneof v1.User.password + */ + password: { + /** + * @generated from field: string password_bcrypt = 2; + */ + value: string; + case: "passwordBcrypt"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.User. + * Use `create(UserSchema)` to create a new message. + */ +export const UserSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 12); + diff --git a/webui/gen/ts/v1/crypto_pb.ts b/webui/gen/ts/v1/crypto_pb.ts new file mode 100644 index 000000000..0ab3ff1cb --- /dev/null +++ b/webui/gen/ts/v1/crypto_pb.ts @@ -0,0 +1,116 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/crypto.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/crypto.proto. + */ +export const file_v1_crypto: GenFile = /*@__PURE__*/ + fileDesc("Cg92MS9jcnlwdG8ucHJvdG8SAnYxIkIKDVNpZ25lZE1lc3NhZ2USDQoFa2V5aWQYASABKAkSDwoHcGF5bG9hZBgCIAEoDBIRCglzaWduYXR1cmUYAyABKAwiIwoQRW5jcnlwdGVkTWVzc2FnZRIPCgdwYXlsb2FkGAEgASgMIjcKCVB1YmxpY0tleRINCgVrZXlpZBgBIAEoCRIbCgdlZDI1NTE5GAIgASgJUgplZDI1NTE5cHViIjkKClByaXZhdGVLZXkSDQoFa2V5aWQYASABKAkSHAoHZWQyNTUxORgCIAEoCVILZWQyNTUxOXByaXZCLFoqZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3YxYgZwcm90bzM"); + +/** + * @generated from message v1.SignedMessage + */ +export type SignedMessage = Message<"v1.SignedMessage"> & { + /** + * a unique identifier generated as the SHA256 of the public key used to sign the message. + * + * @generated from field: string keyid = 1; + */ + keyid: string; + + /** + * the payload + * + * @generated from field: bytes payload = 2; + */ + payload: Uint8Array; + + /** + * the signature of the payload + * + * @generated from field: bytes signature = 3; + */ + signature: Uint8Array; +}; + +/** + * Describes the message v1.SignedMessage. + * Use `create(SignedMessageSchema)` to create a new message. + */ +export const SignedMessageSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_crypto, 0); + +/** + * @generated from message v1.EncryptedMessage + */ +export type EncryptedMessage = Message<"v1.EncryptedMessage"> & { + /** + * @generated from field: bytes payload = 1; + */ + payload: Uint8Array; +}; + +/** + * Describes the message v1.EncryptedMessage. + * Use `create(EncryptedMessageSchema)` to create a new message. + */ +export const EncryptedMessageSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_crypto, 1); + +/** + * @generated from message v1.PublicKey + */ +export type PublicKey = Message<"v1.PublicKey"> & { + /** + * a unique identifier generated as the SHA256 of the public key. + * + * @generated from field: string keyid = 1; + */ + keyid: string; + + /** + * base64 encoded public key + * + * @generated from field: string ed25519 = 2 [json_name = "ed25519pub"]; + */ + ed25519: string; +}; + +/** + * Describes the message v1.PublicKey. + * Use `create(PublicKeySchema)` to create a new message. + */ +export const PublicKeySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_crypto, 2); + +/** + * @generated from message v1.PrivateKey + */ +export type PrivateKey = Message<"v1.PrivateKey"> & { + /** + * a unique identifier generated as the SHA256 of the public key. + * + * @generated from field: string keyid = 1; + */ + keyid: string; + + /** + * base64 encoded private key + * + * @generated from field: string ed25519 = 2 [json_name = "ed25519priv"]; + */ + ed25519: string; +}; + +/** + * Describes the message v1.PrivateKey. + * Use `create(PrivateKeySchema)` to create a new message. + */ +export const PrivateKeySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_crypto, 3); + diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts new file mode 100644 index 000000000..6ca31f044 --- /dev/null +++ b/webui/gen/ts/v1/operations_pb.ts @@ -0,0 +1,608 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/operations.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { BackupProgressEntry, BackupProgressError, RepoStats, ResticSnapshot, RestoreProgressEntry } from "./restic_pb"; +import { file_v1_restic } from "./restic_pb"; +import type { Hook_Condition, RetentionPolicy } from "./config_pb"; +import { file_v1_config } from "./config_pb"; +import type { Empty, Int64List } from "../types/value_pb"; +import { file_types_value } from "../types/value_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/operations.proto. + */ +export const file_v1_operations: GenFile = /*@__PURE__*/ + fileDesc("ChN2MS9vcGVyYXRpb25zLnByb3RvEgJ2MSIyCg1PcGVyYXRpb25MaXN0EiEKCm9wZXJhdGlvbnMYASADKAsyDS52MS5PcGVyYXRpb24inwYKCU9wZXJhdGlvbhIKCgJpZBgBIAEoAxITCgtvcmlnaW5hbF9pZBgNIAEoAxINCgVtb2RubxgMIAEoAxIPCgdmbG93X2lkGAogASgDEhgKEG9yaWdpbmFsX2Zsb3dfaWQYDiABKAMSDwoHcmVwb19pZBgCIAEoCRIRCglyZXBvX2d1aWQYDyABKAkSDwoHcGxhbl9pZBgDIAEoCRITCgtpbnN0YW5jZV9pZBgLIAEoCRITCgtzbmFwc2hvdF9pZBgIIAEoCRIjCgZzdGF0dXMYBCABKA4yEy52MS5PcGVyYXRpb25TdGF0dXMSGgoSdW5peF90aW1lX3N0YXJ0X21zGAUgASgDEhgKEHVuaXhfdGltZV9lbmRfbXMYBiABKAMSFwoPZGlzcGxheV9tZXNzYWdlGAcgASgJEg4KBmxvZ3JlZhgJIAEoCRIvChBvcGVyYXRpb25fYmFja3VwGGQgASgLMhMudjEuT3BlcmF0aW9uQmFja3VwSAASPgoYb3BlcmF0aW9uX2luZGV4X3NuYXBzaG90GGUgASgLMhoudjEuT3BlcmF0aW9uSW5kZXhTbmFwc2hvdEgAEi8KEG9wZXJhdGlvbl9mb3JnZXQYZiABKAsyEy52MS5PcGVyYXRpb25Gb3JnZXRIABItCg9vcGVyYXRpb25fcHJ1bmUYZyABKAsyEi52MS5PcGVyYXRpb25QcnVuZUgAEjEKEW9wZXJhdGlvbl9yZXN0b3JlGGggASgLMhQudjEuT3BlcmF0aW9uUmVzdG9yZUgAEi0KD29wZXJhdGlvbl9zdGF0cxhpIAEoCzISLnYxLk9wZXJhdGlvblN0YXRzSAASMgoSb3BlcmF0aW9uX3J1bl9ob29rGGogASgLMhQudjEuT3BlcmF0aW9uUnVuSG9va0gAEi0KD29wZXJhdGlvbl9jaGVjaxhrIAEoCzISLnYxLk9wZXJhdGlvbkNoZWNrSAASOAoVb3BlcmF0aW9uX3J1bl9jb21tYW5kGGwgASgLMhcudjEuT3BlcmF0aW9uUnVuQ29tbWFuZEgAQgQKAm9wIs8BCg5PcGVyYXRpb25FdmVudBIiCgprZWVwX2FsaXZlGAEgASgLMgwudHlwZXMuRW1wdHlIABIvChJjcmVhdGVkX29wZXJhdGlvbnMYAiABKAsyES52MS5PcGVyYXRpb25MaXN0SAASLwoSdXBkYXRlZF9vcGVyYXRpb25zGAMgASgLMhEudjEuT3BlcmF0aW9uTGlzdEgAEi4KEmRlbGV0ZWRfb3BlcmF0aW9ucxgEIAEoCzIQLnR5cGVzLkludDY0TGlzdEgAQgcKBWV2ZW50ImgKD09wZXJhdGlvbkJhY2t1cBIsCgtsYXN0X3N0YXR1cxgDIAEoCzIXLnYxLkJhY2t1cFByb2dyZXNzRW50cnkSJwoGZXJyb3JzGAQgAygLMhcudjEuQmFja3VwUHJvZ3Jlc3NFcnJvciJOChZPcGVyYXRpb25JbmRleFNuYXBzaG90EiQKCHNuYXBzaG90GAIgASgLMhIudjEuUmVzdGljU25hcHNob3QSDgoGZm9yZ290GAMgASgIIloKD09wZXJhdGlvbkZvcmdldBIiCgZmb3JnZXQYASADKAsyEi52MS5SZXN0aWNTbmFwc2hvdBIjCgZwb2xpY3kYAiABKAsyEy52MS5SZXRlbnRpb25Qb2xpY3kiOwoOT3BlcmF0aW9uUHJ1bmUSEgoGb3V0cHV0GAEgASgJQgIYARIVCg1vdXRwdXRfbG9ncmVmGAIgASgJIjsKDk9wZXJhdGlvbkNoZWNrEhIKBm91dHB1dBgBIAEoCUICGAESFQoNb3V0cHV0X2xvZ3JlZhgCIAEoCSJYChNPcGVyYXRpb25SdW5Db21tYW5kEg8KB2NvbW1hbmQYASABKAkSFQoNb3V0cHV0X2xvZ3JlZhgCIAEoCRIZChFvdXRwdXRfc2l6ZV9ieXRlcxgDIAEoAyJfChBPcGVyYXRpb25SZXN0b3JlEgwKBHBhdGgYASABKAkSDgoGdGFyZ2V0GAIgASgJEi0KC2xhc3Rfc3RhdHVzGAMgASgLMhgudjEuUmVzdG9yZVByb2dyZXNzRW50cnkiLgoOT3BlcmF0aW9uU3RhdHMSHAoFc3RhdHMYASABKAsyDS52MS5SZXBvU3RhdHMicQoQT3BlcmF0aW9uUnVuSG9vaxIRCglwYXJlbnRfb3AYBCABKAMSDAoEbmFtZRgBIAEoCRIVCg1vdXRwdXRfbG9ncmVmGAIgASgJEiUKCWNvbmRpdGlvbhgDIAEoDjISLnYxLkhvb2suQ29uZGl0aW9uKmAKEk9wZXJhdGlvbkV2ZW50VHlwZRIRCg1FVkVOVF9VTktOT1dOEAASEQoNRVZFTlRfQ1JFQVRFRBABEhEKDUVWRU5UX1VQREFURUQQAhIRCg1FVkVOVF9ERUxFVEVEEAMqwgEKD09wZXJhdGlvblN0YXR1cxISCg5TVEFUVVNfVU5LTk9XThAAEhIKDlNUQVRVU19QRU5ESU5HEAESFQoRU1RBVFVTX0lOUFJPR1JFU1MQAhISCg5TVEFUVVNfU1VDQ0VTUxADEhIKDlNUQVRVU19XQVJOSU5HEAcSEAoMU1RBVFVTX0VSUk9SEAQSGwoXU1RBVFVTX1NZU1RFTV9DQU5DRUxMRUQQBRIZChVTVEFUVVNfVVNFUl9DQU5DRUxMRUQQBkIsWipnaXRodWIuY29tL2dhcmV0aGdlb3JnZS9iYWNrcmVzdC9nZW4vZ28vdjFiBnByb3RvMw", [file_v1_restic, file_v1_config, file_types_value]); + +/** + * @generated from message v1.OperationList + */ +export type OperationList = Message<"v1.OperationList"> & { + /** + * @generated from field: repeated v1.Operation operations = 1; + */ + operations: Operation[]; +}; + +/** + * Describes the message v1.OperationList. + * Use `create(OperationListSchema)` to create a new message. + */ +export const OperationListSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 0); + +/** + * @generated from message v1.Operation + */ +export type Operation = Message<"v1.Operation"> & { + /** + * required, primary ID of the operation. ID is sequential based on creation time of the operation. + * + * @generated from field: int64 id = 1; + */ + id: bigint; + + /** + * @generated from field: int64 original_id = 13; + */ + originalId: bigint; + + /** + * modno increments with each change to the operation. This supports easy diffing. + * + * @generated from field: int64 modno = 12; + */ + modno: bigint; + + /** + * flow id groups operations together, e.g. by an execution of a plan. + * must be unique within the context of a repo. + * + * @generated from field: int64 flow_id = 10; + */ + flowId: bigint; + + /** + * @generated from field: int64 original_flow_id = 14; + */ + originalFlowId: bigint; + + /** + * repo id is a string identifier for the repo, and repo_guid is the globally unique ID of the repo. + * + * @generated from field: string repo_id = 2; + */ + repoId: string; + + /** + * @generated from field: string repo_guid = 15; + */ + repoGuid: string; + + /** + * plan id e.g. a scheduled set of operations (or system) that created this operation. + * + * @generated from field: string plan_id = 3; + */ + planId: string; + + /** + * instance ID that created the operation + * + * @generated from field: string instance_id = 11; + */ + instanceId: string; + + /** + * optional snapshot id if associated with a snapshot. + * + * @generated from field: string snapshot_id = 8; + */ + snapshotId: string; + + /** + * @generated from field: v1.OperationStatus status = 4; + */ + status: OperationStatus; + + /** + * required, unix time in milliseconds of the operation's creation (ID is derived from this) + * + * @generated from field: int64 unix_time_start_ms = 5; + */ + unixTimeStartMs: bigint; + + /** + * ptional, unix time in milliseconds of the operation's completion + * + * @generated from field: int64 unix_time_end_ms = 6; + */ + unixTimeEndMs: bigint; + + /** + * optional, human readable context message, typically an error message. + * + * @generated from field: string display_message = 7; + */ + displayMessage: string; + + /** + * logref can point to arbitrary logs associated with the operation. + * + * @generated from field: string logref = 9; + */ + logref: string; + + /** + * @generated from oneof v1.Operation.op + */ + op: { + /** + * @generated from field: v1.OperationBackup operation_backup = 100; + */ + value: OperationBackup; + case: "operationBackup"; + } | { + /** + * @generated from field: v1.OperationIndexSnapshot operation_index_snapshot = 101; + */ + value: OperationIndexSnapshot; + case: "operationIndexSnapshot"; + } | { + /** + * @generated from field: v1.OperationForget operation_forget = 102; + */ + value: OperationForget; + case: "operationForget"; + } | { + /** + * @generated from field: v1.OperationPrune operation_prune = 103; + */ + value: OperationPrune; + case: "operationPrune"; + } | { + /** + * @generated from field: v1.OperationRestore operation_restore = 104; + */ + value: OperationRestore; + case: "operationRestore"; + } | { + /** + * @generated from field: v1.OperationStats operation_stats = 105; + */ + value: OperationStats; + case: "operationStats"; + } | { + /** + * @generated from field: v1.OperationRunHook operation_run_hook = 106; + */ + value: OperationRunHook; + case: "operationRunHook"; + } | { + /** + * @generated from field: v1.OperationCheck operation_check = 107; + */ + value: OperationCheck; + case: "operationCheck"; + } | { + /** + * @generated from field: v1.OperationRunCommand operation_run_command = 108; + */ + value: OperationRunCommand; + case: "operationRunCommand"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.Operation. + * Use `create(OperationSchema)` to create a new message. + */ +export const OperationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 1); + +/** + * OperationEvent is used in the wireformat to stream operation changes to clients + * + * @generated from message v1.OperationEvent + */ +export type OperationEvent = Message<"v1.OperationEvent"> & { + /** + * @generated from oneof v1.OperationEvent.event + */ + event: { + /** + * @generated from field: types.Empty keep_alive = 1; + */ + value: Empty; + case: "keepAlive"; + } | { + /** + * @generated from field: v1.OperationList created_operations = 2; + */ + value: OperationList; + case: "createdOperations"; + } | { + /** + * @generated from field: v1.OperationList updated_operations = 3; + */ + value: OperationList; + case: "updatedOperations"; + } | { + /** + * @generated from field: types.Int64List deleted_operations = 4; + */ + value: Int64List; + case: "deletedOperations"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.OperationEvent. + * Use `create(OperationEventSchema)` to create a new message. + */ +export const OperationEventSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 2); + +/** + * @generated from message v1.OperationBackup + */ +export type OperationBackup = Message<"v1.OperationBackup"> & { + /** + * @generated from field: v1.BackupProgressEntry last_status = 3; + */ + lastStatus?: BackupProgressEntry; + + /** + * @generated from field: repeated v1.BackupProgressError errors = 4; + */ + errors: BackupProgressError[]; +}; + +/** + * Describes the message v1.OperationBackup. + * Use `create(OperationBackupSchema)` to create a new message. + */ +export const OperationBackupSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 3); + +/** + * OperationIndexSnapshot tracks that a snapshot was detected by backrest. + * + * @generated from message v1.OperationIndexSnapshot + */ +export type OperationIndexSnapshot = Message<"v1.OperationIndexSnapshot"> & { + /** + * the snapshot that was indexed. + * + * @generated from field: v1.ResticSnapshot snapshot = 2; + */ + snapshot?: ResticSnapshot; + + /** + * tracks whether this snapshot is forgotten yet. + * + * @generated from field: bool forgot = 3; + */ + forgot: boolean; +}; + +/** + * Describes the message v1.OperationIndexSnapshot. + * Use `create(OperationIndexSnapshotSchema)` to create a new message. + */ +export const OperationIndexSnapshotSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 4); + +/** + * OperationForget tracks a forget operation. + * + * @generated from message v1.OperationForget + */ +export type OperationForget = Message<"v1.OperationForget"> & { + /** + * @generated from field: repeated v1.ResticSnapshot forget = 1; + */ + forget: ResticSnapshot[]; + + /** + * @generated from field: v1.RetentionPolicy policy = 2; + */ + policy?: RetentionPolicy; +}; + +/** + * Describes the message v1.OperationForget. + * Use `create(OperationForgetSchema)` to create a new message. + */ +export const OperationForgetSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 5); + +/** + * OperationPrune tracks a prune operation. + * + * @generated from message v1.OperationPrune + */ +export type OperationPrune = Message<"v1.OperationPrune"> & { + /** + * output of the prune. + * + * @generated from field: string output = 1 [deprecated = true]; + * @deprecated + */ + output: string; + + /** + * logref of the prune output. + * + * @generated from field: string output_logref = 2; + */ + outputLogref: string; +}; + +/** + * Describes the message v1.OperationPrune. + * Use `create(OperationPruneSchema)` to create a new message. + */ +export const OperationPruneSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 6); + +/** + * OperationCheck tracks a check operation. + * + * @generated from message v1.OperationCheck + */ +export type OperationCheck = Message<"v1.OperationCheck"> & { + /** + * output of the check operation. + * + * @generated from field: string output = 1 [deprecated = true]; + * @deprecated + */ + output: string; + + /** + * logref of the check output. + * + * @generated from field: string output_logref = 2; + */ + outputLogref: string; +}; + +/** + * Describes the message v1.OperationCheck. + * Use `create(OperationCheckSchema)` to create a new message. + */ +export const OperationCheckSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 7); + +/** + * OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. + * + * @generated from message v1.OperationRunCommand + */ +export type OperationRunCommand = Message<"v1.OperationRunCommand"> & { + /** + * @generated from field: string command = 1; + */ + command: string; + + /** + * @generated from field: string output_logref = 2; + */ + outputLogref: string; + + /** + * not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. + * + * @generated from field: int64 output_size_bytes = 3; + */ + outputSizeBytes: bigint; +}; + +/** + * Describes the message v1.OperationRunCommand. + * Use `create(OperationRunCommandSchema)` to create a new message. + */ +export const OperationRunCommandSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 8); + +/** + * OperationRestore tracks a restore operation. + * + * @generated from message v1.OperationRestore + */ +export type OperationRestore = Message<"v1.OperationRestore"> & { + /** + * path in the snapshot to restore. + * + * @generated from field: string path = 1; + */ + path: string; + + /** + * location to restore it to. + * + * @generated from field: string target = 2; + */ + target: string; + + /** + * status of the restore. + * + * @generated from field: v1.RestoreProgressEntry last_status = 3; + */ + lastStatus?: RestoreProgressEntry; +}; + +/** + * Describes the message v1.OperationRestore. + * Use `create(OperationRestoreSchema)` to create a new message. + */ +export const OperationRestoreSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 9); + +/** + * OperationStats tracks a stats operation. + * + * @generated from message v1.OperationStats + */ +export type OperationStats = Message<"v1.OperationStats"> & { + /** + * @generated from field: v1.RepoStats stats = 1; + */ + stats?: RepoStats; +}; + +/** + * Describes the message v1.OperationStats. + * Use `create(OperationStatsSchema)` to create a new message. + */ +export const OperationStatsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 10); + +/** + * OperationRunHook tracks a hook that was run. + * + * @generated from message v1.OperationRunHook + */ +export type OperationRunHook = Message<"v1.OperationRunHook"> & { + /** + * ID of the operation that ran the hook. + * + * @generated from field: int64 parent_op = 4; + */ + parentOp: bigint; + + /** + * description of the hook that was run. typically repo/hook_idx or plan/hook_idx. + * + * @generated from field: string name = 1; + */ + name: string; + + /** + * logref of the hook's output. DEPRECATED. + * + * @generated from field: string output_logref = 2; + */ + outputLogref: string; + + /** + * triggering condition of the hook. + * + * @generated from field: v1.Hook.Condition condition = 3; + */ + condition: Hook_Condition; +}; + +/** + * Describes the message v1.OperationRunHook. + * Use `create(OperationRunHookSchema)` to create a new message. + */ +export const OperationRunHookSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_operations, 11); + +/** + * OperationEventType indicates whether the operation was created or updated + * + * @generated from enum v1.OperationEventType + */ +export enum OperationEventType { + /** + * @generated from enum value: EVENT_UNKNOWN = 0; + */ + EVENT_UNKNOWN = 0, + + /** + * @generated from enum value: EVENT_CREATED = 1; + */ + EVENT_CREATED = 1, + + /** + * @generated from enum value: EVENT_UPDATED = 2; + */ + EVENT_UPDATED = 2, + + /** + * @generated from enum value: EVENT_DELETED = 3; + */ + EVENT_DELETED = 3, +} + +/** + * Describes the enum v1.OperationEventType. + */ +export const OperationEventTypeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_operations, 0); + +/** + * @generated from enum v1.OperationStatus + */ +export enum OperationStatus { + /** + * used to indicate that the status is unknown. + * + * @generated from enum value: STATUS_UNKNOWN = 0; + */ + STATUS_UNKNOWN = 0, + + /** + * used to indicate that the operation is pending. + * + * @generated from enum value: STATUS_PENDING = 1; + */ + STATUS_PENDING = 1, + + /** + * used to indicate that the operation is in progress. + * + * @generated from enum value: STATUS_INPROGRESS = 2; + */ + STATUS_INPROGRESS = 2, + + /** + * used to indicate that the operation completed successfully. + * + * @generated from enum value: STATUS_SUCCESS = 3; + */ + STATUS_SUCCESS = 3, + + /** + * used to indicate that the operation completed with warnings. + * + * @generated from enum value: STATUS_WARNING = 7; + */ + STATUS_WARNING = 7, + + /** + * used to indicate that the operation failed. + * + * @generated from enum value: STATUS_ERROR = 4; + */ + STATUS_ERROR = 4, + + /** + * indicates operation cancelled by the system. + * + * @generated from enum value: STATUS_SYSTEM_CANCELLED = 5; + */ + STATUS_SYSTEM_CANCELLED = 5, + + /** + * indicates operation cancelled by the user. + * + * @generated from enum value: STATUS_USER_CANCELLED = 6; + */ + STATUS_USER_CANCELLED = 6, +} + +/** + * Describes the enum v1.OperationStatus. + */ +export const OperationStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_operations, 1); + diff --git a/webui/gen/ts/v1/restic_pb.ts b/webui/gen/ts/v1/restic_pb.ts new file mode 100644 index 000000000..106cf6702 --- /dev/null +++ b/webui/gen/ts/v1/restic_pb.ts @@ -0,0 +1,449 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/restic.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/restic.proto. + */ +export const file_v1_restic: GenFile = /*@__PURE__*/ + fileDesc("Cg92MS9yZXN0aWMucHJvdG8SAnYxIrcBCg5SZXN0aWNTbmFwc2hvdBIKCgJpZBgBIAEoCRIUCgx1bml4X3RpbWVfbXMYAiABKAMSEAoIaG9zdG5hbWUYAyABKAkSEAoIdXNlcm5hbWUYBCABKAkSDAoEdHJlZRgFIAEoCRIOCgZwYXJlbnQYBiABKAkSDQoFcGF0aHMYByADKAkSDAoEdGFncxgIIAMoCRIkCgdzdW1tYXJ5GAkgASgLMhMudjEuU25hcHNob3RTdW1tYXJ5IqgCCg9TbmFwc2hvdFN1bW1hcnkSEQoJZmlsZXNfbmV3GAEgASgDEhUKDWZpbGVzX2NoYW5nZWQYAiABKAMSGAoQZmlsZXNfdW5tb2RpZmllZBgDIAEoAxIQCghkaXJzX25ldxgEIAEoAxIUCgxkaXJzX2NoYW5nZWQYBSABKAMSFwoPZGlyc191bm1vZGlmaWVkGAYgASgDEhIKCmRhdGFfYmxvYnMYByABKAMSEgoKdHJlZV9ibG9icxgIIAEoAxISCgpkYXRhX2FkZGVkGAkgASgDEh0KFXRvdGFsX2ZpbGVzX3Byb2Nlc3NlZBgKIAEoAxIdChV0b3RhbF9ieXRlc19wcm9jZXNzZWQYCyABKAMSFgoOdG90YWxfZHVyYXRpb24YDCABKAEiOwoSUmVzdGljU25hcHNob3RMaXN0EiUKCXNuYXBzaG90cxgBIAMoCzISLnYxLlJlc3RpY1NuYXBzaG90In0KE0JhY2t1cFByb2dyZXNzRW50cnkSLwoGc3RhdHVzGAEgASgLMh0udjEuQmFja3VwUHJvZ3Jlc3NTdGF0dXNFbnRyeUgAEiwKB3N1bW1hcnkYAiABKAsyGS52MS5CYWNrdXBQcm9ncmVzc1N1bW1hcnlIAEIHCgVlbnRyeSKZAQoZQmFja3VwUHJvZ3Jlc3NTdGF0dXNFbnRyeRIUCgxwZXJjZW50X2RvbmUYASABKAESEwoLdG90YWxfZmlsZXMYAiABKAMSEwoLdG90YWxfYnl0ZXMYAyABKAMSEgoKZmlsZXNfZG9uZRgEIAEoAxISCgpieXRlc19kb25lGAUgASgDEhQKDGN1cnJlbnRfZmlsZRgGIAMoCSLDAgoVQmFja3VwUHJvZ3Jlc3NTdW1tYXJ5EhEKCWZpbGVzX25ldxgBIAEoAxIVCg1maWxlc19jaGFuZ2VkGAIgASgDEhgKEGZpbGVzX3VubW9kaWZpZWQYAyABKAMSEAoIZGlyc19uZXcYBCABKAMSFAoMZGlyc19jaGFuZ2VkGAUgASgDEhcKD2RpcnNfdW5tb2RpZmllZBgGIAEoAxISCgpkYXRhX2Jsb2JzGAcgASgDEhIKCnRyZWVfYmxvYnMYCCABKAMSEgoKZGF0YV9hZGRlZBgJIAEoAxIdChV0b3RhbF9maWxlc19wcm9jZXNzZWQYCiABKAMSHQoVdG90YWxfYnl0ZXNfcHJvY2Vzc2VkGAsgASgDEhYKDnRvdGFsX2R1cmF0aW9uGAwgASgBEhMKC3NuYXBzaG90X2lkGA0gASgJIkQKE0JhY2t1cFByb2dyZXNzRXJyb3ISDAoEaXRlbRgBIAEoCRIOCgZkdXJpbmcYAiABKAkSDwoHbWVzc2FnZRgDIAEoCSK1AQoUUmVzdG9yZVByb2dyZXNzRW50cnkSFAoMbWVzc2FnZV90eXBlGAEgASgJEhcKD3NlY29uZHNfZWxhcHNlZBgCIAEoARITCgt0b3RhbF9ieXRlcxgDIAEoAxIWCg5ieXRlc19yZXN0b3JlZBgEIAEoAxITCgt0b3RhbF9maWxlcxgFIAEoAxIWCg5maWxlc19yZXN0b3JlZBgGIAEoAxIUCgxwZXJjZW50X2RvbmUYByABKAEijQEKCVJlcG9TdGF0cxISCgp0b3RhbF9zaXplGAEgASgDEh8KF3RvdGFsX3VuY29tcHJlc3NlZF9zaXplGAIgASgDEhkKEWNvbXByZXNzaW9uX3JhdGlvGAMgASgBEhgKEHRvdGFsX2Jsb2JfY291bnQYBSABKAMSFgoOc25hcHNob3RfY291bnQYBiABKANCLFoqZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3YxYgZwcm90bzM"); + +/** + * ResticSnapshot represents a restic snapshot. + * + * @generated from message v1.ResticSnapshot + */ +export type ResticSnapshot = Message<"v1.ResticSnapshot"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: int64 unix_time_ms = 2; + */ + unixTimeMs: bigint; + + /** + * @generated from field: string hostname = 3; + */ + hostname: string; + + /** + * @generated from field: string username = 4; + */ + username: string; + + /** + * tree hash + * + * @generated from field: string tree = 5; + */ + tree: string; + + /** + * parent snapshot's id + * + * @generated from field: string parent = 6; + */ + parent: string; + + /** + * @generated from field: repeated string paths = 7; + */ + paths: string[]; + + /** + * @generated from field: repeated string tags = 8; + */ + tags: string[]; + + /** + * added in 0.17.0 restic outputs the summary in the snapshot + * + * @generated from field: v1.SnapshotSummary summary = 9; + */ + summary?: SnapshotSummary; +}; + +/** + * Describes the message v1.ResticSnapshot. + * Use `create(ResticSnapshotSchema)` to create a new message. + */ +export const ResticSnapshotSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 0); + +/** + * @generated from message v1.SnapshotSummary + */ +export type SnapshotSummary = Message<"v1.SnapshotSummary"> & { + /** + * @generated from field: int64 files_new = 1; + */ + filesNew: bigint; + + /** + * @generated from field: int64 files_changed = 2; + */ + filesChanged: bigint; + + /** + * @generated from field: int64 files_unmodified = 3; + */ + filesUnmodified: bigint; + + /** + * @generated from field: int64 dirs_new = 4; + */ + dirsNew: bigint; + + /** + * @generated from field: int64 dirs_changed = 5; + */ + dirsChanged: bigint; + + /** + * @generated from field: int64 dirs_unmodified = 6; + */ + dirsUnmodified: bigint; + + /** + * @generated from field: int64 data_blobs = 7; + */ + dataBlobs: bigint; + + /** + * @generated from field: int64 tree_blobs = 8; + */ + treeBlobs: bigint; + + /** + * @generated from field: int64 data_added = 9; + */ + dataAdded: bigint; + + /** + * @generated from field: int64 total_files_processed = 10; + */ + totalFilesProcessed: bigint; + + /** + * @generated from field: int64 total_bytes_processed = 11; + */ + totalBytesProcessed: bigint; + + /** + * @generated from field: double total_duration = 12; + */ + totalDuration: number; +}; + +/** + * Describes the message v1.SnapshotSummary. + * Use `create(SnapshotSummarySchema)` to create a new message. + */ +export const SnapshotSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 1); + +/** + * ResticSnapshotList represents a list of restic snapshots. + * + * @generated from message v1.ResticSnapshotList + */ +export type ResticSnapshotList = Message<"v1.ResticSnapshotList"> & { + /** + * @generated from field: repeated v1.ResticSnapshot snapshots = 1; + */ + snapshots: ResticSnapshot[]; +}; + +/** + * Describes the message v1.ResticSnapshotList. + * Use `create(ResticSnapshotListSchema)` to create a new message. + */ +export const ResticSnapshotListSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 2); + +/** + * BackupProgressEntriy represents a single entry in the backup progress stream. + * + * @generated from message v1.BackupProgressEntry + */ +export type BackupProgressEntry = Message<"v1.BackupProgressEntry"> & { + /** + * @generated from oneof v1.BackupProgressEntry.entry + */ + entry: { + /** + * @generated from field: v1.BackupProgressStatusEntry status = 1; + */ + value: BackupProgressStatusEntry; + case: "status"; + } | { + /** + * @generated from field: v1.BackupProgressSummary summary = 2; + */ + value: BackupProgressSummary; + case: "summary"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.BackupProgressEntry. + * Use `create(BackupProgressEntrySchema)` to create a new message. + */ +export const BackupProgressEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 3); + +/** + * BackupProgressStatusEntry represents a single status entry in the backup progress stream. + * + * @generated from message v1.BackupProgressStatusEntry + */ +export type BackupProgressStatusEntry = Message<"v1.BackupProgressStatusEntry"> & { + /** + * See https://restic.readthedocs.io/en/stable/075_scripting.html#id1 + * + * 0.0 - 1.0 + * + * @generated from field: double percent_done = 1; + */ + percentDone: number; + + /** + * @generated from field: int64 total_files = 2; + */ + totalFiles: bigint; + + /** + * @generated from field: int64 total_bytes = 3; + */ + totalBytes: bigint; + + /** + * @generated from field: int64 files_done = 4; + */ + filesDone: bigint; + + /** + * @generated from field: int64 bytes_done = 5; + */ + bytesDone: bigint; + + /** + * @generated from field: repeated string current_file = 6; + */ + currentFile: string[]; +}; + +/** + * Describes the message v1.BackupProgressStatusEntry. + * Use `create(BackupProgressStatusEntrySchema)` to create a new message. + */ +export const BackupProgressStatusEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 4); + +/** + * BackupProgressSummary represents a the summary event emitted at the end of a backup stream. + * + * @generated from message v1.BackupProgressSummary + */ +export type BackupProgressSummary = Message<"v1.BackupProgressSummary"> & { + /** + * See https://restic.readthedocs.io/en/stable/075_scripting.html#summary + * + * @generated from field: int64 files_new = 1; + */ + filesNew: bigint; + + /** + * @generated from field: int64 files_changed = 2; + */ + filesChanged: bigint; + + /** + * @generated from field: int64 files_unmodified = 3; + */ + filesUnmodified: bigint; + + /** + * @generated from field: int64 dirs_new = 4; + */ + dirsNew: bigint; + + /** + * @generated from field: int64 dirs_changed = 5; + */ + dirsChanged: bigint; + + /** + * @generated from field: int64 dirs_unmodified = 6; + */ + dirsUnmodified: bigint; + + /** + * @generated from field: int64 data_blobs = 7; + */ + dataBlobs: bigint; + + /** + * @generated from field: int64 tree_blobs = 8; + */ + treeBlobs: bigint; + + /** + * @generated from field: int64 data_added = 9; + */ + dataAdded: bigint; + + /** + * @generated from field: int64 total_files_processed = 10; + */ + totalFilesProcessed: bigint; + + /** + * @generated from field: int64 total_bytes_processed = 11; + */ + totalBytesProcessed: bigint; + + /** + * @generated from field: double total_duration = 12; + */ + totalDuration: number; + + /** + * @generated from field: string snapshot_id = 13; + */ + snapshotId: string; +}; + +/** + * Describes the message v1.BackupProgressSummary. + * Use `create(BackupProgressSummarySchema)` to create a new message. + */ +export const BackupProgressSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 5); + +/** + * @generated from message v1.BackupProgressError + */ +export type BackupProgressError = Message<"v1.BackupProgressError"> & { + /** + * See https://restic.readthedocs.io/en/stable/075_scripting.html#error + * + * @generated from field: string item = 1; + */ + item: string; + + /** + * @generated from field: string during = 2; + */ + during: string; + + /** + * @generated from field: string message = 3; + */ + message: string; +}; + +/** + * Describes the message v1.BackupProgressError. + * Use `create(BackupProgressErrorSchema)` to create a new message. + */ +export const BackupProgressErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 6); + +/** + * RestoreProgressEvent represents a single entry in the restore progress stream. + * + * @generated from message v1.RestoreProgressEntry + */ +export type RestoreProgressEntry = Message<"v1.RestoreProgressEntry"> & { + /** + * "summary" or "status" + * + * @generated from field: string message_type = 1; + */ + messageType: string; + + /** + * @generated from field: double seconds_elapsed = 2; + */ + secondsElapsed: number; + + /** + * @generated from field: int64 total_bytes = 3; + */ + totalBytes: bigint; + + /** + * @generated from field: int64 bytes_restored = 4; + */ + bytesRestored: bigint; + + /** + * @generated from field: int64 total_files = 5; + */ + totalFiles: bigint; + + /** + * @generated from field: int64 files_restored = 6; + */ + filesRestored: bigint; + + /** + * 0.0 - 1.0 + * + * @generated from field: double percent_done = 7; + */ + percentDone: number; +}; + +/** + * Describes the message v1.RestoreProgressEntry. + * Use `create(RestoreProgressEntrySchema)` to create a new message. + */ +export const RestoreProgressEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 7); + +/** + * @generated from message v1.RepoStats + */ +export type RepoStats = Message<"v1.RepoStats"> & { + /** + * @generated from field: int64 total_size = 1; + */ + totalSize: bigint; + + /** + * @generated from field: int64 total_uncompressed_size = 2; + */ + totalUncompressedSize: bigint; + + /** + * @generated from field: double compression_ratio = 3; + */ + compressionRatio: number; + + /** + * @generated from field: int64 total_blob_count = 5; + */ + totalBlobCount: bigint; + + /** + * @generated from field: int64 snapshot_count = 6; + */ + snapshotCount: bigint; +}; + +/** + * Describes the message v1.RepoStats. + * Use `create(RepoStatsSchema)` to create a new message. + */ +export const RepoStatsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_restic, 8); + diff --git a/webui/gen/ts/v1/service_pb.ts b/webui/gen/ts/v1/service_pb.ts new file mode 100644 index 000000000..988d88f1f --- /dev/null +++ b/webui/gen/ts/v1/service_pb.ts @@ -0,0 +1,743 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/service.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { ConfigSchema, RepoSchema } from "./config_pb"; +import { file_v1_config } from "./config_pb"; +import type { ResticSnapshotListSchema } from "./restic_pb"; +import { file_v1_restic } from "./restic_pb"; +import type { OperationEventSchema, OperationListSchema, OperationStatus } from "./operations_pb"; +import { file_v1_operations } from "./operations_pb"; +import type { BoolValueSchema, BytesValueSchema, Int64ValueSchema, StringListSchema, StringValueSchema } from "../types/value_pb"; +import { file_types_value } from "../types/value_pb"; +import type { EmptySchema } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import { file_google_api_annotations } from "../google/api/annotations_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/service.proto. + */ +export const file_v1_service: GenFile = /*@__PURE__*/ + fileDesc("ChB2MS9zZXJ2aWNlLnByb3RvEgJ2MSLXAQoKT3BTZWxlY3RvchILCgNpZHMYASADKAMSGAoLaW5zdGFuY2VfaWQYBiABKAlIAIgBARIWCglyZXBvX2d1aWQYByABKAlIAYgBARIUCgdwbGFuX2lkGAMgASgJSAKIAQESGAoLc25hcHNob3RfaWQYBCABKAlIA4gBARIUCgdmbG93X2lkGAUgASgDSASIAQFCDgoMX2luc3RhbmNlX2lkQgwKCl9yZXBvX2d1aWRCCgoIX3BsYW5faWRCDgoMX3NuYXBzaG90X2lkQgoKCF9mbG93X2lkIsABChFEb1JlcG9UYXNrUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEigKBHRhc2sYAiABKA4yGi52MS5Eb1JlcG9UYXNrUmVxdWVzdC5UYXNrInAKBFRhc2sSDQoJVEFTS19OT05FEAASGAoUVEFTS19JTkRFWF9TTkFQU0hPVFMQARIOCgpUQVNLX1BSVU5FEAISDgoKVEFTS19DSEVDSxADEg4KClRBU0tfU1RBVFMQBBIPCgtUQVNLX1VOTE9DSxAFIkwKE0NsZWFySGlzdG9yeVJlcXVlc3QSIAoIc2VsZWN0b3IYASABKAsyDi52MS5PcFNlbGVjdG9yEhMKC29ubHlfZmFpbGVkGAIgASgIIkYKDUZvcmdldFJlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdwbGFuX2lkGAIgASgJEhMKC3NuYXBzaG90X2lkGAMgASgJIjgKFExpc3RTbmFwc2hvdHNSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSDwoHcGxhbl9pZBgCIAEoCSJIChRHZXRPcGVyYXRpb25zUmVxdWVzdBIgCghzZWxlY3RvchgBIAEoCzIOLnYxLk9wU2VsZWN0b3ISDgoGbGFzdF9uGAIgASgDIm0KFlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QSDwoHcGxhbl9pZBgBIAEoCRIPCgdyZXBvX2lkGAUgASgJEhMKC3NuYXBzaG90X2lkGAIgASgJEgwKBHBhdGgYAyABKAkSDgoGdGFyZ2V0GAQgASgJIk4KGExpc3RTbmFwc2hvdEZpbGVzUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEhMKC3NuYXBzaG90X2lkGAIgASgJEgwKBHBhdGgYAyABKAkiRwoZTGlzdFNuYXBzaG90RmlsZXNSZXNwb25zZRIMCgRwYXRoGAEgASgJEhwKB2VudHJpZXMYAiADKAsyCy52MS5Mc0VudHJ5Ih0KDkxvZ0RhdGFSZXF1ZXN0EgsKA3JlZhgBIAEoCSKWAQoHTHNFbnRyeRIMCgRuYW1lGAEgASgJEgwKBHR5cGUYAiABKAkSDAoEcGF0aBgDIAEoCRILCgN1aWQYBCABKAMSCwoDZ2lkGAUgASgDEgwKBHNpemUYBiABKAMSDAoEbW9kZRgHIAEoAxINCgVtdGltZRgIIAEoCRINCgVhdGltZRgJIAEoCRINCgVjdGltZRgKIAEoCSI1ChFSdW5Db21tYW5kUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEg8KB2NvbW1hbmQYAiABKAkitQUKGFN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZRI8Cg5yZXBvX3N1bW1hcmllcxgBIAMoCzIkLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5TdW1tYXJ5EjwKDnBsYW5fc3VtbWFyaWVzGAIgAygLMiQudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLlN1bW1hcnkSEwoLY29uZmlnX3BhdGgYCiABKAkSEQoJZGF0YV9wYXRoGAsgASgJGu4CCgdTdW1tYXJ5EgoKAmlkGAEgASgJEh0KFWJhY2t1cHNfZmFpbGVkXzMwZGF5cxgCIAEoAxIjChtiYWNrdXBzX3dhcm5pbmdfbGFzdF8zMGRheXMYAyABKAMSIwobYmFja3Vwc19zdWNjZXNzX2xhc3RfMzBkYXlzGAQgASgDEiEKGWJ5dGVzX3NjYW5uZWRfbGFzdF8zMGRheXMYBSABKAMSHwoXYnl0ZXNfYWRkZWRfbGFzdF8zMGRheXMYBiABKAMSFwoPdG90YWxfc25hcHNob3RzGAcgASgDEhkKEWJ5dGVzX3NjYW5uZWRfYXZnGAggASgDEhcKD2J5dGVzX2FkZGVkX2F2ZxgJIAEoAxIbChNuZXh0X2JhY2t1cF90aW1lX21zGAogASgDEkAKDnJlY2VudF9iYWNrdXBzGAsgASgLMigudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLkJhY2t1cENoYXJ0GoMBCgtCYWNrdXBDaGFydBIPCgdmbG93X2lkGAEgAygDEhQKDHRpbWVzdGFtcF9tcxgCIAMoAxITCgtkdXJhdGlvbl9tcxgDIAMoAxIjCgZzdGF0dXMYBCADKA4yEy52MS5PcGVyYXRpb25TdGF0dXMSEwoLYnl0ZXNfYWRkZWQYBSADKAMypwkKCEJhY2tyZXN0EjEKCUdldENvbmZpZxIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoKLnYxLkNvbmZpZyIAEiUKCVNldENvbmZpZxIKLnYxLkNvbmZpZxoKLnYxLkNvbmZpZyIAEi8KD0NoZWNrUmVwb0V4aXN0cxIILnYxLlJlcG8aEC50eXBlcy5Cb29sVmFsdWUiABIhCgdBZGRSZXBvEggudjEuUmVwbxoKLnYxLkNvbmZpZyIAEi4KClJlbW92ZVJlcG8SEi50eXBlcy5TdHJpbmdWYWx1ZRoKLnYxLkNvbmZpZyIAEkQKEkdldE9wZXJhdGlvbkV2ZW50cxIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoSLnYxLk9wZXJhdGlvbkV2ZW50IgAwARI+Cg1HZXRPcGVyYXRpb25zEhgudjEuR2V0T3BlcmF0aW9uc1JlcXVlc3QaES52MS5PcGVyYXRpb25MaXN0IgASQwoNTGlzdFNuYXBzaG90cxIYLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0GhYudjEuUmVzdGljU25hcHNob3RMaXN0IgASUgoRTGlzdFNuYXBzaG90RmlsZXMSHC52MS5MaXN0U25hcHNob3RGaWxlc1JlcXVlc3QaHS52MS5MaXN0U25hcHNob3RGaWxlc1Jlc3BvbnNlIgASNgoGQmFja3VwEhIudHlwZXMuU3RyaW5nVmFsdWUaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI9CgpEb1JlcG9UYXNrEhUudjEuRG9SZXBvVGFza1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI1CgZGb3JnZXQSES52MS5Gb3JnZXRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASPwoHUmVzdG9yZRIaLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI1CgZDYW5jZWwSES50eXBlcy5JbnQ2NFZhbHVlGhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASNAoHR2V0TG9ncxISLnYxLkxvZ0RhdGFSZXF1ZXN0GhEudHlwZXMuQnl0ZXNWYWx1ZSIAMAESOAoKUnVuQ29tbWFuZBIVLnYxLlJ1bkNvbW1hbmRSZXF1ZXN0GhEudHlwZXMuSW50NjRWYWx1ZSIAEjkKDkdldERvd25sb2FkVVJMEhEudHlwZXMuSW50NjRWYWx1ZRoSLnR5cGVzLlN0cmluZ1ZhbHVlIgASQQoMQ2xlYXJIaXN0b3J5EhcudjEuQ2xlYXJIaXN0b3J5UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAEjsKEFBhdGhBdXRvY29tcGxldGUSEi50eXBlcy5TdHJpbmdWYWx1ZRoRLnR5cGVzLlN0cmluZ0xpc3QiABJNChNHZXRTdW1tYXJ5RGFzaGJvYXJkEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5GhwudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlIgBCLFoqZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3YxYgZwcm90bzM", [file_v1_config, file_v1_restic, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); + +/** + * OpSelector is a message that can be used to select operations e.g. by query. + * + * @generated from message v1.OpSelector + */ +export type OpSelector = Message<"v1.OpSelector"> & { + /** + * @generated from field: repeated int64 ids = 1; + */ + ids: bigint[]; + + /** + * @generated from field: optional string instance_id = 6; + */ + instanceId?: string; + + /** + * @generated from field: optional string repo_guid = 7; + */ + repoGuid?: string; + + /** + * @generated from field: optional string plan_id = 3; + */ + planId?: string; + + /** + * @generated from field: optional string snapshot_id = 4; + */ + snapshotId?: string; + + /** + * @generated from field: optional int64 flow_id = 5; + */ + flowId?: bigint; +}; + +/** + * Describes the message v1.OpSelector. + * Use `create(OpSelectorSchema)` to create a new message. + */ +export const OpSelectorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 0); + +/** + * @generated from message v1.DoRepoTaskRequest + */ +export type DoRepoTaskRequest = Message<"v1.DoRepoTaskRequest"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; + + /** + * @generated from field: v1.DoRepoTaskRequest.Task task = 2; + */ + task: DoRepoTaskRequest_Task; +}; + +/** + * Describes the message v1.DoRepoTaskRequest. + * Use `create(DoRepoTaskRequestSchema)` to create a new message. + */ +export const DoRepoTaskRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 1); + +/** + * @generated from enum v1.DoRepoTaskRequest.Task + */ +export enum DoRepoTaskRequest_Task { + /** + * @generated from enum value: TASK_NONE = 0; + */ + NONE = 0, + + /** + * @generated from enum value: TASK_INDEX_SNAPSHOTS = 1; + */ + INDEX_SNAPSHOTS = 1, + + /** + * @generated from enum value: TASK_PRUNE = 2; + */ + PRUNE = 2, + + /** + * @generated from enum value: TASK_CHECK = 3; + */ + CHECK = 3, + + /** + * @generated from enum value: TASK_STATS = 4; + */ + STATS = 4, + + /** + * @generated from enum value: TASK_UNLOCK = 5; + */ + UNLOCK = 5, +} + +/** + * Describes the enum v1.DoRepoTaskRequest.Task. + */ +export const DoRepoTaskRequest_TaskSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_service, 1, 0); + +/** + * @generated from message v1.ClearHistoryRequest + */ +export type ClearHistoryRequest = Message<"v1.ClearHistoryRequest"> & { + /** + * @generated from field: v1.OpSelector selector = 1; + */ + selector?: OpSelector; + + /** + * @generated from field: bool only_failed = 2; + */ + onlyFailed: boolean; +}; + +/** + * Describes the message v1.ClearHistoryRequest. + * Use `create(ClearHistoryRequestSchema)` to create a new message. + */ +export const ClearHistoryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 2); + +/** + * @generated from message v1.ForgetRequest + */ +export type ForgetRequest = Message<"v1.ForgetRequest"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; + + /** + * @generated from field: string plan_id = 2; + */ + planId: string; + + /** + * @generated from field: string snapshot_id = 3; + */ + snapshotId: string; +}; + +/** + * Describes the message v1.ForgetRequest. + * Use `create(ForgetRequestSchema)` to create a new message. + */ +export const ForgetRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 3); + +/** + * @generated from message v1.ListSnapshotsRequest + */ +export type ListSnapshotsRequest = Message<"v1.ListSnapshotsRequest"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; + + /** + * @generated from field: string plan_id = 2; + */ + planId: string; +}; + +/** + * Describes the message v1.ListSnapshotsRequest. + * Use `create(ListSnapshotsRequestSchema)` to create a new message. + */ +export const ListSnapshotsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 4); + +/** + * @generated from message v1.GetOperationsRequest + */ +export type GetOperationsRequest = Message<"v1.GetOperationsRequest"> & { + /** + * @generated from field: v1.OpSelector selector = 1; + */ + selector?: OpSelector; + + /** + * limit to the last n operations + * + * @generated from field: int64 last_n = 2; + */ + lastN: bigint; +}; + +/** + * Describes the message v1.GetOperationsRequest. + * Use `create(GetOperationsRequestSchema)` to create a new message. + */ +export const GetOperationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 5); + +/** + * @generated from message v1.RestoreSnapshotRequest + */ +export type RestoreSnapshotRequest = Message<"v1.RestoreSnapshotRequest"> & { + /** + * @generated from field: string plan_id = 1; + */ + planId: string; + + /** + * @generated from field: string repo_id = 5; + */ + repoId: string; + + /** + * @generated from field: string snapshot_id = 2; + */ + snapshotId: string; + + /** + * @generated from field: string path = 3; + */ + path: string; + + /** + * @generated from field: string target = 4; + */ + target: string; +}; + +/** + * Describes the message v1.RestoreSnapshotRequest. + * Use `create(RestoreSnapshotRequestSchema)` to create a new message. + */ +export const RestoreSnapshotRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 6); + +/** + * @generated from message v1.ListSnapshotFilesRequest + */ +export type ListSnapshotFilesRequest = Message<"v1.ListSnapshotFilesRequest"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; + + /** + * @generated from field: string snapshot_id = 2; + */ + snapshotId: string; + + /** + * @generated from field: string path = 3; + */ + path: string; +}; + +/** + * Describes the message v1.ListSnapshotFilesRequest. + * Use `create(ListSnapshotFilesRequestSchema)` to create a new message. + */ +export const ListSnapshotFilesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 7); + +/** + * @generated from message v1.ListSnapshotFilesResponse + */ +export type ListSnapshotFilesResponse = Message<"v1.ListSnapshotFilesResponse"> & { + /** + * @generated from field: string path = 1; + */ + path: string; + + /** + * @generated from field: repeated v1.LsEntry entries = 2; + */ + entries: LsEntry[]; +}; + +/** + * Describes the message v1.ListSnapshotFilesResponse. + * Use `create(ListSnapshotFilesResponseSchema)` to create a new message. + */ +export const ListSnapshotFilesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 8); + +/** + * @generated from message v1.LogDataRequest + */ +export type LogDataRequest = Message<"v1.LogDataRequest"> & { + /** + * @generated from field: string ref = 1; + */ + ref: string; +}; + +/** + * Describes the message v1.LogDataRequest. + * Use `create(LogDataRequestSchema)` to create a new message. + */ +export const LogDataRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 9); + +/** + * @generated from message v1.LsEntry + */ +export type LsEntry = Message<"v1.LsEntry"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: string type = 2; + */ + type: string; + + /** + * @generated from field: string path = 3; + */ + path: string; + + /** + * @generated from field: int64 uid = 4; + */ + uid: bigint; + + /** + * @generated from field: int64 gid = 5; + */ + gid: bigint; + + /** + * @generated from field: int64 size = 6; + */ + size: bigint; + + /** + * @generated from field: int64 mode = 7; + */ + mode: bigint; + + /** + * @generated from field: string mtime = 8; + */ + mtime: string; + + /** + * @generated from field: string atime = 9; + */ + atime: string; + + /** + * @generated from field: string ctime = 10; + */ + ctime: string; +}; + +/** + * Describes the message v1.LsEntry. + * Use `create(LsEntrySchema)` to create a new message. + */ +export const LsEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 10); + +/** + * @generated from message v1.RunCommandRequest + */ +export type RunCommandRequest = Message<"v1.RunCommandRequest"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; + + /** + * @generated from field: string command = 2; + */ + command: string; +}; + +/** + * Describes the message v1.RunCommandRequest. + * Use `create(RunCommandRequestSchema)` to create a new message. + */ +export const RunCommandRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 11); + +/** + * @generated from message v1.SummaryDashboardResponse + */ +export type SummaryDashboardResponse = Message<"v1.SummaryDashboardResponse"> & { + /** + * @generated from field: repeated v1.SummaryDashboardResponse.Summary repo_summaries = 1; + */ + repoSummaries: SummaryDashboardResponse_Summary[]; + + /** + * @generated from field: repeated v1.SummaryDashboardResponse.Summary plan_summaries = 2; + */ + planSummaries: SummaryDashboardResponse_Summary[]; + + /** + * @generated from field: string config_path = 10; + */ + configPath: string; + + /** + * @generated from field: string data_path = 11; + */ + dataPath: string; +}; + +/** + * Describes the message v1.SummaryDashboardResponse. + * Use `create(SummaryDashboardResponseSchema)` to create a new message. + */ +export const SummaryDashboardResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 12); + +/** + * @generated from message v1.SummaryDashboardResponse.Summary + */ +export type SummaryDashboardResponse_Summary = Message<"v1.SummaryDashboardResponse.Summary"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: int64 backups_failed_30days = 2; + */ + backupsFailed30days: bigint; + + /** + * @generated from field: int64 backups_warning_last_30days = 3; + */ + backupsWarningLast30days: bigint; + + /** + * @generated from field: int64 backups_success_last_30days = 4; + */ + backupsSuccessLast30days: bigint; + + /** + * @generated from field: int64 bytes_scanned_last_30days = 5; + */ + bytesScannedLast30days: bigint; + + /** + * @generated from field: int64 bytes_added_last_30days = 6; + */ + bytesAddedLast30days: bigint; + + /** + * @generated from field: int64 total_snapshots = 7; + */ + totalSnapshots: bigint; + + /** + * @generated from field: int64 bytes_scanned_avg = 8; + */ + bytesScannedAvg: bigint; + + /** + * @generated from field: int64 bytes_added_avg = 9; + */ + bytesAddedAvg: bigint; + + /** + * @generated from field: int64 next_backup_time_ms = 10; + */ + nextBackupTimeMs: bigint; + + /** + * Charts + * + * recent backups + * + * @generated from field: v1.SummaryDashboardResponse.BackupChart recent_backups = 11; + */ + recentBackups?: SummaryDashboardResponse_BackupChart; +}; + +/** + * Describes the message v1.SummaryDashboardResponse.Summary. + * Use `create(SummaryDashboardResponse_SummarySchema)` to create a new message. + */ +export const SummaryDashboardResponse_SummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 12, 0); + +/** + * @generated from message v1.SummaryDashboardResponse.BackupChart + */ +export type SummaryDashboardResponse_BackupChart = Message<"v1.SummaryDashboardResponse.BackupChart"> & { + /** + * @generated from field: repeated int64 flow_id = 1; + */ + flowId: bigint[]; + + /** + * @generated from field: repeated int64 timestamp_ms = 2; + */ + timestampMs: bigint[]; + + /** + * @generated from field: repeated int64 duration_ms = 3; + */ + durationMs: bigint[]; + + /** + * @generated from field: repeated v1.OperationStatus status = 4; + */ + status: OperationStatus[]; + + /** + * @generated from field: repeated int64 bytes_added = 5; + */ + bytesAdded: bigint[]; +}; + +/** + * Describes the message v1.SummaryDashboardResponse.BackupChart. + * Use `create(SummaryDashboardResponse_BackupChartSchema)` to create a new message. + */ +export const SummaryDashboardResponse_BackupChartSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 12, 1); + +/** + * @generated from service v1.Backrest + */ +export const Backrest: GenService<{ + /** + * @generated from rpc v1.Backrest.GetConfig + */ + getConfig: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof ConfigSchema; + }, + /** + * @generated from rpc v1.Backrest.SetConfig + */ + setConfig: { + methodKind: "unary"; + input: typeof ConfigSchema; + output: typeof ConfigSchema; + }, + /** + * @generated from rpc v1.Backrest.CheckRepoExists + */ + checkRepoExists: { + methodKind: "unary"; + input: typeof RepoSchema; + output: typeof BoolValueSchema; + }, + /** + * @generated from rpc v1.Backrest.AddRepo + */ + addRepo: { + methodKind: "unary"; + input: typeof RepoSchema; + output: typeof ConfigSchema; + }, + /** + * @generated from rpc v1.Backrest.RemoveRepo + */ + removeRepo: { + methodKind: "unary"; + input: typeof StringValueSchema; + output: typeof ConfigSchema; + }, + /** + * @generated from rpc v1.Backrest.GetOperationEvents + */ + getOperationEvents: { + methodKind: "server_streaming"; + input: typeof EmptySchema; + output: typeof OperationEventSchema; + }, + /** + * @generated from rpc v1.Backrest.GetOperations + */ + getOperations: { + methodKind: "unary"; + input: typeof GetOperationsRequestSchema; + output: typeof OperationListSchema; + }, + /** + * @generated from rpc v1.Backrest.ListSnapshots + */ + listSnapshots: { + methodKind: "unary"; + input: typeof ListSnapshotsRequestSchema; + output: typeof ResticSnapshotListSchema; + }, + /** + * @generated from rpc v1.Backrest.ListSnapshotFiles + */ + listSnapshotFiles: { + methodKind: "unary"; + input: typeof ListSnapshotFilesRequestSchema; + output: typeof ListSnapshotFilesResponseSchema; + }, + /** + * Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + * + * @generated from rpc v1.Backrest.Backup + */ + backup: { + methodKind: "unary"; + input: typeof StringValueSchema; + output: typeof EmptySchema; + }, + /** + * DoRepoTask schedules a repo task. It accepts a repo id and a task type and returns empty if the task is enqueued. + * + * @generated from rpc v1.Backrest.DoRepoTask + */ + doRepoTask: { + methodKind: "unary"; + input: typeof DoRepoTaskRequestSchema; + output: typeof EmptySchema; + }, + /** + * Forget schedules a forget operation. It accepts a plan id and returns empty if the task is enqueued. + * + * @generated from rpc v1.Backrest.Forget + */ + forget: { + methodKind: "unary"; + input: typeof ForgetRequestSchema; + output: typeof EmptySchema; + }, + /** + * Restore schedules a restore operation. + * + * @generated from rpc v1.Backrest.Restore + */ + restore: { + methodKind: "unary"; + input: typeof RestoreSnapshotRequestSchema; + output: typeof EmptySchema; + }, + /** + * Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + * + * @generated from rpc v1.Backrest.Cancel + */ + cancel: { + methodKind: "unary"; + input: typeof Int64ValueSchema; + output: typeof EmptySchema; + }, + /** + * GetLogs returns the keyed large data for the given operation. + * + * @generated from rpc v1.Backrest.GetLogs + */ + getLogs: { + methodKind: "server_streaming"; + input: typeof LogDataRequestSchema; + output: typeof BytesValueSchema; + }, + /** + * RunCommand executes a generic restic command on the repository. + * + * @generated from rpc v1.Backrest.RunCommand + */ + runCommand: { + methodKind: "unary"; + input: typeof RunCommandRequestSchema; + output: typeof Int64ValueSchema; + }, + /** + * GetDownloadURL returns a signed download URL given a forget operation ID. + * + * @generated from rpc v1.Backrest.GetDownloadURL + */ + getDownloadURL: { + methodKind: "unary"; + input: typeof Int64ValueSchema; + output: typeof StringValueSchema; + }, + /** + * Clears the history of operations + * + * @generated from rpc v1.Backrest.ClearHistory + */ + clearHistory: { + methodKind: "unary"; + input: typeof ClearHistoryRequestSchema; + output: typeof EmptySchema; + }, + /** + * PathAutocomplete provides path autocompletion options for a given filesystem path. + * + * @generated from rpc v1.Backrest.PathAutocomplete + */ + pathAutocomplete: { + methodKind: "unary"; + input: typeof StringValueSchema; + output: typeof StringListSchema; + }, + /** + * GetSummaryDashboard returns data for the dashboard view. + * + * @generated from rpc v1.Backrest.GetSummaryDashboard + */ + getSummaryDashboard: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof SummaryDashboardResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_v1_service, 0); + diff --git a/webui/gen/ts/v1/syncservice_pb.ts b/webui/gen/ts/v1/syncservice_pb.ts new file mode 100644 index 000000000..72d3ca4a2 --- /dev/null +++ b/webui/gen/ts/v1/syncservice_pb.ts @@ -0,0 +1,446 @@ +// @generated by protoc-gen-es v2.2.2 with parameter "target=ts" +// @generated from file v1/syncservice.proto (package v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import { file_v1_config } from "./config_pb"; +import type { PublicKey, SignedMessage } from "./crypto_pb"; +import { file_v1_crypto } from "./crypto_pb"; +import { file_v1_restic } from "./restic_pb"; +import type { OpSelector } from "./service_pb"; +import { file_v1_service } from "./service_pb"; +import type { OperationEvent } from "./operations_pb"; +import { file_v1_operations } from "./operations_pb"; +import { file_types_value } from "../types/value_pb"; +import type { EmptySchema } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import { file_google_api_annotations } from "../google/api/annotations_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file v1/syncservice.proto. + */ +export const file_v1_syncservice: GenFile = /*@__PURE__*/ + fileDesc("ChR2MS9zeW5jc2VydmljZS5wcm90bxICdjEikgEKFkdldFJlbW90ZVJlcG9zUmVzcG9uc2USPAoFcmVwb3MYASADKAsyLS52MS5HZXRSZW1vdGVSZXBvc1Jlc3BvbnNlLlJlbW90ZVJlcG9NZXRhZGF0YRo6ChJSZW1vdGVSZXBvTWV0YWRhdGESEwoLaW5zdGFuY2VfaWQYASABKAkSDwoHcmVwb19pZBgCIAEoCSK/CQoOU3luY1N0cmVhbUl0ZW0SKwoOc2lnbmVkX21lc3NhZ2UYASABKAsyES52MS5TaWduZWRNZXNzYWdlSAASOwoJaGFuZHNoYWtlGAMgASgLMiYudjEuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvbkhhbmRzaGFrZUgAEkYKD2RpZmZfb3BlcmF0aW9ucxgUIAEoCzIrLnYxLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25EaWZmT3BlcmF0aW9uc0gAEkYKD3NlbmRfb3BlcmF0aW9ucxgVIAEoCzIrLnYxLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25TZW5kT3BlcmF0aW9uc0gAEj4KC3NlbmRfY29uZmlnGBYgASgLMicudjEuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblNlbmRDb25maWdIABJPChdlc3RhYmxpc2hfc2hhcmVkX3NlY3JldBgXIAEoCzIsLnYxLlN5bmNTdHJlYW1JdGVtLlN5bmNFc3RhYmxpc2hTaGFyZWRTZWNyZXRIABI6Cgh0aHJvdHRsZRjoByABKAsyJS52MS5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uVGhyb3R0bGVIABp6ChNTeW5jQWN0aW9uSGFuZHNoYWtlEhgKEHByb3RvY29sX3ZlcnNpb24YASABKAMSIQoKcHVibGljX2tleRgCIAEoCzINLnYxLlB1YmxpY0tleRImCgtpbnN0YW5jZV9pZBgDIAEoCzIRLnYxLlNpZ25lZE1lc3NhZ2UaOAoUU3luY0FjdGlvblNlbmRDb25maWcSIAoGY29uZmlnGAEgASgLMhAudjEuUmVtb3RlQ29uZmlnGigKFVN5bmNBY3Rpb25Db25uZWN0UmVwbxIPCgdyZXBvX2lkGAEgASgJGqMBChhTeW5jQWN0aW9uRGlmZk9wZXJhdGlvbnMSMAoYaGF2ZV9vcGVyYXRpb25zX3NlbGVjdG9yGAEgASgLMg4udjEuT3BTZWxlY3RvchIaChJoYXZlX29wZXJhdGlvbl9pZHMYAiADKAMSHQoVaGF2ZV9vcGVyYXRpb25fbW9kbm9zGAMgAygDEhoKEnJlcXVlc3Rfb3BlcmF0aW9ucxgEIAMoAxo9ChhTeW5jQWN0aW9uU2VuZE9wZXJhdGlvbnMSIQoFZXZlbnQYASABKAsyEi52MS5PcGVyYXRpb25FdmVudBomChJTeW5jQWN0aW9uVGhyb3R0bGUSEAoIZGVsYXlfbXMYASABKAMaOAoZU3luY0VzdGFibGlzaFNoYXJlZFNlY3JldBIbCgdlZDI1NTE5GAIgASgJUgplZDI1NTE5cHViIrQBChNSZXBvQ29ubmVjdGlvblN0YXRlEhwKGENPTk5FQ1RJT05fU1RBVEVfVU5LTk9XThAAEhwKGENPTk5FQ1RJT05fU1RBVEVfUEVORElORxABEh4KGkNPTk5FQ1RJT05fU1RBVEVfQ09OTkVDVEVEEAISIQodQ09OTkVDVElPTl9TVEFURV9VTkFVVEhPUklaRUQQAxIeChpDT05ORUNUSU9OX1NUQVRFX05PVF9GT1VORBAEQggKBmFjdGlvbiItCgxSZW1vdGVDb25maWcSHQoFcmVwb3MYASADKAsyDi52MS5SZW1vdGVSZXBvImEKClJlbW90ZVJlcG8SCgoCaWQYASABKAkSDAoEZ3VpZBgLIAEoCRILCgN1cmkYAiABKAkSEAoIcGFzc3dvcmQYAyABKAkSCwoDZW52GAQgAygJEg0KBWZsYWdzGAUgAygJKvsBChNTeW5jQ29ubmVjdGlvblN0YXRlEhwKGENPTk5FQ1RJT05fU1RBVEVfVU5LTk9XThAAEhwKGENPTk5FQ1RJT05fU1RBVEVfUEVORElORxABEh4KGkNPTk5FQ1RJT05fU1RBVEVfQ09OTkVDVEVEEAISIQodQ09OTkVDVElPTl9TVEFURV9ESVNDT05ORUNURUQQAxIfChtDT05ORUNUSU9OX1NUQVRFX1JFVFJZX1dBSVQQBBIfChtDT05ORUNUSU9OX1NUQVRFX0VSUk9SX0FVVEgQChIjCh9DT05ORUNUSU9OX1NUQVRFX0VSUk9SX1BST1RPQ09MEAsykwEKE0JhY2tyZXN0U3luY1NlcnZpY2USNAoEU3luYxISLnYxLlN5bmNTdHJlYW1JdGVtGhIudjEuU3luY1N0cmVhbUl0ZW0iACgBMAESRgoOR2V0UmVtb3RlUmVwb3MSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaGi52MS5HZXRSZW1vdGVSZXBvc1Jlc3BvbnNlIgBCLFoqZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3YxYgZwcm90bzM", [file_v1_config, file_v1_crypto, file_v1_restic, file_v1_service, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); + +/** + * @generated from message v1.GetRemoteReposResponse + */ +export type GetRemoteReposResponse = Message<"v1.GetRemoteReposResponse"> & { + /** + * @generated from field: repeated v1.GetRemoteReposResponse.RemoteRepoMetadata repos = 1; + */ + repos: GetRemoteReposResponse_RemoteRepoMetadata[]; +}; + +/** + * Describes the message v1.GetRemoteReposResponse. + * Use `create(GetRemoteReposResponseSchema)` to create a new message. + */ +export const GetRemoteReposResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 0); + +/** + * @generated from message v1.GetRemoteReposResponse.RemoteRepoMetadata + */ +export type GetRemoteReposResponse_RemoteRepoMetadata = Message<"v1.GetRemoteReposResponse.RemoteRepoMetadata"> & { + /** + * @generated from field: string instance_id = 1; + */ + instanceId: string; + + /** + * @generated from field: string repo_id = 2; + */ + repoId: string; +}; + +/** + * Describes the message v1.GetRemoteReposResponse.RemoteRepoMetadata. + * Use `create(GetRemoteReposResponse_RemoteRepoMetadataSchema)` to create a new message. + */ +export const GetRemoteReposResponse_RemoteRepoMetadataSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 0, 0); + +/** + * @generated from message v1.SyncStreamItem + */ +export type SyncStreamItem = Message<"v1.SyncStreamItem"> & { + /** + * @generated from oneof v1.SyncStreamItem.action + */ + action: { + /** + * @generated from field: v1.SignedMessage signed_message = 1; + */ + value: SignedMessage; + case: "signedMessage"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncActionHandshake handshake = 3; + */ + value: SyncStreamItem_SyncActionHandshake; + case: "handshake"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncActionDiffOperations diff_operations = 20; + */ + value: SyncStreamItem_SyncActionDiffOperations; + case: "diffOperations"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncActionSendOperations send_operations = 21; + */ + value: SyncStreamItem_SyncActionSendOperations; + case: "sendOperations"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncActionSendConfig send_config = 22; + */ + value: SyncStreamItem_SyncActionSendConfig; + case: "sendConfig"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncEstablishSharedSecret establish_shared_secret = 23; + */ + value: SyncStreamItem_SyncEstablishSharedSecret; + case: "establishSharedSecret"; + } | { + /** + * @generated from field: v1.SyncStreamItem.SyncActionThrottle throttle = 1000; + */ + value: SyncStreamItem_SyncActionThrottle; + case: "throttle"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message v1.SyncStreamItem. + * Use `create(SyncStreamItemSchema)` to create a new message. + */ +export const SyncStreamItemSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1); + +/** + * @generated from message v1.SyncStreamItem.SyncActionHandshake + */ +export type SyncStreamItem_SyncActionHandshake = Message<"v1.SyncStreamItem.SyncActionHandshake"> & { + /** + * @generated from field: int64 protocol_version = 1; + */ + protocolVersion: bigint; + + /** + * @generated from field: v1.PublicKey public_key = 2; + */ + publicKey?: PublicKey; + + /** + * @generated from field: v1.SignedMessage instance_id = 3; + */ + instanceId?: SignedMessage; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionHandshake. + * Use `create(SyncStreamItem_SyncActionHandshakeSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionHandshakeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 0); + +/** + * @generated from message v1.SyncStreamItem.SyncActionSendConfig + */ +export type SyncStreamItem_SyncActionSendConfig = Message<"v1.SyncStreamItem.SyncActionSendConfig"> & { + /** + * @generated from field: v1.RemoteConfig config = 1; + */ + config?: RemoteConfig; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionSendConfig. + * Use `create(SyncStreamItem_SyncActionSendConfigSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionSendConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 1); + +/** + * @generated from message v1.SyncStreamItem.SyncActionConnectRepo + */ +export type SyncStreamItem_SyncActionConnectRepo = Message<"v1.SyncStreamItem.SyncActionConnectRepo"> & { + /** + * @generated from field: string repo_id = 1; + */ + repoId: string; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionConnectRepo. + * Use `create(SyncStreamItem_SyncActionConnectRepoSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionConnectRepoSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 2); + +/** + * @generated from message v1.SyncStreamItem.SyncActionDiffOperations + */ +export type SyncStreamItem_SyncActionDiffOperations = Message<"v1.SyncStreamItem.SyncActionDiffOperations"> & { + /** + * Client connects and sends a list of "have_operations" that exist in its log. + * have_operation_ids and have_operation_modnos are the operation IDs and modnos that the client has when zip'd pairwise. + * + * @generated from field: v1.OpSelector have_operations_selector = 1; + */ + haveOperationsSelector?: OpSelector; + + /** + * @generated from field: repeated int64 have_operation_ids = 2; + */ + haveOperationIds: bigint[]; + + /** + * @generated from field: repeated int64 have_operation_modnos = 3; + */ + haveOperationModnos: bigint[]; + + /** + * Server sends a list of "request_operations" for any operations that it doesn't have. + * + * @generated from field: repeated int64 request_operations = 4; + */ + requestOperations: bigint[]; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionDiffOperations. + * Use `create(SyncStreamItem_SyncActionDiffOperationsSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionDiffOperationsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 3); + +/** + * @generated from message v1.SyncStreamItem.SyncActionSendOperations + */ +export type SyncStreamItem_SyncActionSendOperations = Message<"v1.SyncStreamItem.SyncActionSendOperations"> & { + /** + * @generated from field: v1.OperationEvent event = 1; + */ + event?: OperationEvent; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionSendOperations. + * Use `create(SyncStreamItem_SyncActionSendOperationsSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionSendOperationsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 4); + +/** + * @generated from message v1.SyncStreamItem.SyncActionThrottle + */ +export type SyncStreamItem_SyncActionThrottle = Message<"v1.SyncStreamItem.SyncActionThrottle"> & { + /** + * @generated from field: int64 delay_ms = 1; + */ + delayMs: bigint; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncActionThrottle. + * Use `create(SyncStreamItem_SyncActionThrottleSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionThrottleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 5); + +/** + * @generated from message v1.SyncStreamItem.SyncEstablishSharedSecret + */ +export type SyncStreamItem_SyncEstablishSharedSecret = Message<"v1.SyncStreamItem.SyncEstablishSharedSecret"> & { + /** + * a one-time-use ed25519 public key with a matching unshared private key. Used to perform a key exchange. + * See https://pkg.go.dev/crypto/ecdh#PrivateKey.ECDH . + * + * base64 encoded public key + * + * @generated from field: string ed25519 = 2 [json_name = "ed25519pub"]; + */ + ed25519: string; +}; + +/** + * Describes the message v1.SyncStreamItem.SyncEstablishSharedSecret. + * Use `create(SyncStreamItem_SyncEstablishSharedSecretSchema)` to create a new message. + */ +export const SyncStreamItem_SyncEstablishSharedSecretSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 1, 6); + +/** + * @generated from enum v1.SyncStreamItem.RepoConnectionState + */ +export enum SyncStreamItem_RepoConnectionState { + /** + * @generated from enum value: CONNECTION_STATE_UNKNOWN = 0; + */ + CONNECTION_STATE_UNKNOWN = 0, + + /** + * queried, response not yet received. + * + * @generated from enum value: CONNECTION_STATE_PENDING = 1; + */ + CONNECTION_STATE_PENDING = 1, + + /** + * @generated from enum value: CONNECTION_STATE_CONNECTED = 2; + */ + CONNECTION_STATE_CONNECTED = 2, + + /** + * @generated from enum value: CONNECTION_STATE_UNAUTHORIZED = 3; + */ + CONNECTION_STATE_UNAUTHORIZED = 3, + + /** + * @generated from enum value: CONNECTION_STATE_NOT_FOUND = 4; + */ + CONNECTION_STATE_NOT_FOUND = 4, +} + +/** + * Describes the enum v1.SyncStreamItem.RepoConnectionState. + */ +export const SyncStreamItem_RepoConnectionStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_syncservice, 1, 0); + +/** + * RemoteConfig contains shareable properties from a remote backrest instance. + * + * @generated from message v1.RemoteConfig + */ +export type RemoteConfig = Message<"v1.RemoteConfig"> & { + /** + * @generated from field: repeated v1.RemoteRepo repos = 1; + */ + repos: RemoteRepo[]; +}; + +/** + * Describes the message v1.RemoteConfig. + * Use `create(RemoteConfigSchema)` to create a new message. + */ +export const RemoteConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 2); + +/** + * @generated from message v1.RemoteRepo + */ +export type RemoteRepo = Message<"v1.RemoteRepo"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string guid = 11; + */ + guid: string; + + /** + * @generated from field: string uri = 2; + */ + uri: string; + + /** + * @generated from field: string password = 3; + */ + password: string; + + /** + * @generated from field: repeated string env = 4; + */ + env: string[]; + + /** + * @generated from field: repeated string flags = 5; + */ + flags: string[]; +}; + +/** + * Describes the message v1.RemoteRepo. + * Use `create(RemoteRepoSchema)` to create a new message. + */ +export const RemoteRepoSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_syncservice, 3); + +/** + * @generated from enum v1.SyncConnectionState + */ +export enum SyncConnectionState { + /** + * @generated from enum value: CONNECTION_STATE_UNKNOWN = 0; + */ + CONNECTION_STATE_UNKNOWN = 0, + + /** + * @generated from enum value: CONNECTION_STATE_PENDING = 1; + */ + CONNECTION_STATE_PENDING = 1, + + /** + * @generated from enum value: CONNECTION_STATE_CONNECTED = 2; + */ + CONNECTION_STATE_CONNECTED = 2, + + /** + * @generated from enum value: CONNECTION_STATE_DISCONNECTED = 3; + */ + CONNECTION_STATE_DISCONNECTED = 3, + + /** + * @generated from enum value: CONNECTION_STATE_RETRY_WAIT = 4; + */ + CONNECTION_STATE_RETRY_WAIT = 4, + + /** + * @generated from enum value: CONNECTION_STATE_ERROR_AUTH = 10; + */ + CONNECTION_STATE_ERROR_AUTH = 10, + + /** + * @generated from enum value: CONNECTION_STATE_ERROR_PROTOCOL = 11; + */ + CONNECTION_STATE_ERROR_PROTOCOL = 11, +} + +/** + * Describes the enum v1.SyncConnectionState. + */ +export const SyncConnectionStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_v1_syncservice, 0); + +/** + * @generated from service v1.BackrestSyncService + */ +export const BackrestSyncService: GenService<{ + /** + * @generated from rpc v1.BackrestSyncService.Sync + */ + sync: { + methodKind: "bidi_streaming"; + input: typeof SyncStreamItemSchema; + output: typeof SyncStreamItemSchema; + }, + /** + * @generated from rpc v1.BackrestSyncService.GetRemoteRepos + */ + getRemoteRepos: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof GetRemoteReposResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_v1_syncservice, 0); + diff --git a/webui/package.json b/webui/package.json index 604d1ba68..3fa23d362 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,11 +1,50 @@ { - "name": "webui", + "name": "backrest", "version": "1.0.0", "description": "", - "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "parcel serve src/index.html", + "clean": "rimraf dist", + "clean-windows": "rimraf dist-windows", + "build": "cross-env UI_OS=unix parcel build src/index.html --public-url ./", + "build-windows": "cross-env UI_OS=windows parcel build src/index.html --dist-dir dist-windows --public-url ./", + "check": "tsc --noEmit" }, "author": "", - "license": "ISC" + "license": "ISC", + "dependencies": { + "@ant-design/icons": "^5.5.1", + "@bufbuild/protobuf": "^2.2.2", + "@connectrpc/connect": "^2.0.0", + "@connectrpc/connect-web": "^2.0.0", + "@parcel/transformer-sass": "^2.13.2", + "@types/lodash": "^4.17.13", + "@types/node": "^20.17.8", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-virtualized": "^9.22.0", + "antd": "^5.22.2", + "buffer": "^6.0.3", + "cross-env": "^7.0.3", + "events": "^3.3.0", + "lodash": "^4.17.21", + "parcel": "^2.13.2", + "process": "^0.11.10", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-js-cron": "^5.0.1", + "react-router": "^6.28.0", + "react-router-dom": "^6.28.0", + "react-virtualized": "^9.22.5", + "recharts": "^2.13.3", + "rimraf": "^5.0.10", + "svgo": "^3.3.2", + "typescript": "^5.7.2" + }, + "@parcel/resolver-default": { + "packageExports": true + }, + "devDependencies": { + "rc-collapse": "^3.9.0" + } } diff --git a/webui/pnpm-lock.yaml b/webui/pnpm-lock.yaml new file mode 100644 index 000000000..6e1757b6e --- /dev/null +++ b/webui/pnpm-lock.yaml @@ -0,0 +1,3865 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/icons': + specifier: ^5.5.1 + version: 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@bufbuild/protobuf': + specifier: ^2.2.2 + version: 2.2.2 + '@connectrpc/connect': + specifier: ^2.0.0 + version: 2.0.0(@bufbuild/protobuf@2.2.2) + '@connectrpc/connect-web': + specifier: ^2.0.0 + version: 2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2)) + '@parcel/transformer-sass': + specifier: ^2.13.2 + version: 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@types/lodash': + specifier: ^4.17.13 + version: 4.17.13 + '@types/node': + specifier: ^20.17.8 + version: 20.17.8 + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + '@types/react-virtualized': + specifier: ^9.22.0 + version: 9.22.0 + antd: + specifier: ^5.22.2 + version: 5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + events: + specifier: ^3.3.0 + version: 3.3.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + parcel: + specifier: ^2.13.2 + version: 2.13.2(@swc/helpers@0.5.15)(svgo@3.3.2)(typescript@5.7.2) + process: + specifier: ^0.11.10 + version: 0.11.10 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-js-cron: + specifier: ^5.0.1 + version: 5.0.1(antd@5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: + specifier: ^6.28.0 + version: 6.28.0(react@18.3.1) + react-router-dom: + specifier: ^6.28.0 + version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtualized: + specifier: ^9.22.5 + version: 9.22.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.13.3 + version: 2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + svgo: + specifier: ^3.3.2 + version: 3.3.2 + typescript: + specifier: ^5.7.2 + version: 5.7.2 + devDependencies: + rc-collapse: + specifier: ^3.9.0 + version: 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + +packages: + + '@ant-design/colors@7.1.0': + resolution: {integrity: sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==} + + '@ant-design/cssinjs-utils@1.1.1': + resolution: {integrity: sha512-2HAiyGGGnM0es40SxdszeQAU5iWp41wBIInq+ONTCKjlSKOrzQfnw4JDtB8IBmqE6tQaEKwmzTP2LGdt5DSwYQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@ant-design/cssinjs@1.22.0': + resolution: {integrity: sha512-W9XSFeRPR0mAN3OuxfuS/xhENCYKf+8s+QyNNER0FSWoK9OpISTag6CCweg6lq0hASQ/2Vcza0Z8/kGivCP0Ng==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/fast-color@2.0.6': + resolution: {integrity: sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==} + engines: {node: '>=8.x'} + + '@ant-design/icons-svg@4.4.2': + resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} + + '@ant-design/icons@5.5.1': + resolution: {integrity: sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/react-slick@1.1.2': + resolution: {integrity: sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==} + peerDependencies: + react: '>=16.9.0' + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.2.2': + resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==} + + '@connectrpc/connect-web@2.0.0': + resolution: {integrity: sha512-oeCxqHXLXlWJdmcvp9L3scgAuK+FjNSn+twyhUxc8yvDbTumnt5Io+LnBzSYxAdUdYqTw5yHfTSCJ4hj0QID0g==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0 + + '@connectrpc/connect@2.0.0': + resolution: {integrity: sha512-Usm8jgaaULANJU8vVnhWssSA6nrZ4DJEAbkNtXSoZay2YD5fDyMukCxu8NEhCvFzfHvrhxhcjttvgpyhOM7xAQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@emotion/unitless@0.7.5': + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + + '@lmdb/lmdb-darwin-arm64@2.8.5': + resolution: {integrity: sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==} + cpu: [arm64] + os: [darwin] + + '@lmdb/lmdb-darwin-x64@2.8.5': + resolution: {integrity: sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==} + cpu: [x64] + os: [darwin] + + '@lmdb/lmdb-linux-arm64@2.8.5': + resolution: {integrity: sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==} + cpu: [arm64] + os: [linux] + + '@lmdb/lmdb-linux-arm@2.8.5': + resolution: {integrity: sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==} + cpu: [arm] + os: [linux] + + '@lmdb/lmdb-linux-x64@2.8.5': + resolution: {integrity: sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==} + cpu: [x64] + os: [linux] + + '@lmdb/lmdb-win32-x64@2.8.5': + resolution: {integrity: sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==} + cpu: [x64] + os: [win32] + + '@mischnic/json-sourcemap@0.1.1': + resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} + engines: {node: '>=12.0.0'} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@parcel/bundler-default@2.13.2': + resolution: {integrity: sha512-WY0LB1B7H6zIGXBtwssZRmzk788GzHoOGvMSIqgE/mZ0+jPF5V54zkjbhPBXj1fvoKOGlFy8Bm/gd/GnlQDdIg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/cache@2.13.2': + resolution: {integrity: sha512-Y0nWlCMWDSp1lxiPI5zCWTGD0InnVZ+IfqeyLWmROAqValYyd0QZCvnSljKJ144jWTr0jXxDveir+DVF8sAYaA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/codeframe@2.13.2': + resolution: {integrity: sha512-qFMiS14orb6QSQj5/J/QN+gJElUfedVAKBTNkp9QB4i8ObdLHDqHRUzFb55ZQJI3G4vsxOOWAOUXGirtLwrxGQ==} + engines: {node: '>= 16.0.0'} + + '@parcel/compressor-raw@2.13.2': + resolution: {integrity: sha512-HX51w7WlgQY2f30p3Le1B5nFsUrtEA1phvWEwQDm1gEC6OPmDrxNsFLWx18JdGlKWTaPYbAGXRMSOjUWU41N9w==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/config-default@2.13.2': + resolution: {integrity: sha512-oTf69/Ikxb7b8uqdu4SasRnIn7e68dCSNW2PhXuBkHq2GgzTSnpSqCwur70wQwrHKHdNt9RtIjLQgC6oOs5aJQ==} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/core@2.13.2': + resolution: {integrity: sha512-1zC5Au4z9or5XyP6ipfvJqHktuB0jD7WuxMcV1CWAZGARHKylLe+0ccl+Wx7HN5O+xAvfCDtTlKrATY8qyrIyw==} + engines: {node: '>= 16.0.0'} + + '@parcel/diagnostic@2.13.2': + resolution: {integrity: sha512-6Au0JEJ5SY2gYrY0/m0i0sTuqTvK0k2E9azhBJR+zzCREbUxLiDdLZ+vXAfLW7t/kPAcWtdNU0Bj7pnZcMiMXg==} + engines: {node: '>= 16.0.0'} + + '@parcel/events@2.13.2': + resolution: {integrity: sha512-BVB9hW1RGh/tMaDHfpa+uIgz5PMULorCnjmWr/KvrlhdUSUQoaPYfRcTDYrKhoKuNIKsWSnTGvXrxE53L5qo0w==} + engines: {node: '>= 16.0.0'} + + '@parcel/feature-flags@2.13.2': + resolution: {integrity: sha512-cCwDAKD4Er24EkuQ+loVZXSURpM0gAGRsLJVoBtFiCSbB3nmIJJ6FLRwSBI/5OsOUExiUXDvSpfUCA5ldGTzbw==} + engines: {node: '>= 16.0.0'} + + '@parcel/fs@2.13.2': + resolution: {integrity: sha512-bdeIMuAXhMnROvqV55JWRUmjD438/T7h3r3NsFnkq+Mp4z2nuAn0STxbqDNxIgTMJHNunSDzncqRNMT7xJCe8A==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/graph@3.3.2': + resolution: {integrity: sha512-aAysQLRr8SOonSHWqdKHMJzfcrDFXKK8IYZEurlOzosiSgZXrAK7q8b8JcaJ4r84/jlvQYNYneNZeFQxKjHXkA==} + engines: {node: '>= 16.0.0'} + + '@parcel/logger@2.13.2': + resolution: {integrity: sha512-SFVABAMqaT9jIDn4maPgaQQauPDz8fpoKUGEuLF44Q0aQFbBUy7vX7KYs/EvYSWZo4VyJcUDHvIInBlepA0/ZQ==} + engines: {node: '>= 16.0.0'} + + '@parcel/markdown-ansi@2.13.2': + resolution: {integrity: sha512-MIEoetfT/snk1GqWzBI3AhifV257i2xke9dvyQl14PPiMl+TlVhwnbQyA09WJBvDor+MuxZypHL7xoFdW8ff3A==} + engines: {node: '>= 16.0.0'} + + '@parcel/namer-default@2.13.2': + resolution: {integrity: sha512-wHaaJZcZEZUaCylC88PqjN4BybJhnkpP5RYg1xGWBTzdxhZthxvDbeOI+0YZ4jZXrLyVNjPyPRwyx0ETlq8MKA==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/node-resolver-core@3.4.2': + resolution: {integrity: sha512-SwnKLcZRG1VdB5JeM/Ax5VMWWh2QfXufmMQCKKx0/Kk41nUpie+aIZKj3LH6Z/fJsnKig/vXpeWoxGhmG523qg==} + engines: {node: '>= 16.0.0'} + + '@parcel/optimizer-css@2.13.2': + resolution: {integrity: sha512-V9JszWd1Lk3b/9hpfRp6U8lfOIaFPyevGFNTrT+CFMviuipCMWrkUgBa7wtFvqN1i8IJ5NV5FhIlc12qfBBBgA==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/optimizer-htmlnano@2.13.2': + resolution: {integrity: sha512-/ikDOZrnO4tdt99h34OyqnNIhugdeqWgnpfqEQ6Xi7odIn8OIGfwAHBXoyKRyUU8YUTqLhzOhckbSMwFTPRmXg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/optimizer-image@2.13.2': + resolution: {integrity: sha512-1BsQOPoSB0TBJJ40TiN+VS3YK2V4EMDtaOML1Bet2oTLMlL77vJG/xT5QHzhExYK+ZyFh2R0gq7deEKXNScBzg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/optimizer-svgo@2.13.2': + resolution: {integrity: sha512-QbuQzGfk5b/p9Yzc8PaSyjwalZbu/5afrKaLYKkiyG+kAVVOGMsxA2WPqPdb8x551AgdQL4OVODS9dE3zdDQOQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/optimizer-swc@2.13.2': + resolution: {integrity: sha512-tyxXn36UAxZkAh+/cjvWwLCBkY+DL7+4G9NHWl5KeFqErc4diBox81Aiu8hnswNzFIg4ljn6f0rNpnWM3yfoMg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/package-manager@2.13.2': + resolution: {integrity: sha512-6HjfbdJUjHyNKzYB7GSYnOCtLwqCGW7yT95GlnnTKyFffvXYsqvBSyepMuPRlbX0mFUm4S9l2DH3OVZrk108AA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/packager-css@2.13.2': + resolution: {integrity: sha512-agao71rIHU1lR776IMwxKvknl1/Yglhkr2qSY0JQC10PRQXHs7nj0GXd69p568W42A3/rkMWrXjWkGzhbAcPRg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/packager-html@2.13.2': + resolution: {integrity: sha512-RHoYR4sp5VZATQbKE2Rn7DrJKK7HnvUTKB0WPFSppWJbJrqrZgvVCqnBMI2FPkbCoznGdt20rQ1R6vs591fuxQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/packager-js@2.13.2': + resolution: {integrity: sha512-/dx19/vTCb4JIx/556hz6KEmwanasUNLAFsZ1PAm5AYDcoxJtHiNITRilA6JTlO+mdsERxOI5eE7tHCTou1ErQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/packager-raw@2.13.2': + resolution: {integrity: sha512-P+BnMZ3WS4F+Kpd+iv6PCfgyCftPGf8iGSQOCPkWb5fjuNjfvIzsq4WAW41FPbu788JwChev1O4zREYzlQjG2Q==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/packager-svg@2.13.2': + resolution: {integrity: sha512-K99yyQ1IsbQlGWYOLaxv/GGeWXDq0snbgGrCJvXFS8APZZ2CrXRm2634XLFkY3XA1cKqP47wz+KbibMT/+uaPQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/packager-wasm@2.13.2': + resolution: {integrity: sha512-XqFQQcQRgZLPHgLWsQmWHr47ebsu9F7hmpHS+hFNHda4zj7WDtw7r7n6/d8CEXzdI3agpxR3XKVZzx7nB6sQig==} + engines: {node: '>=16.0.0', parcel: ^2.13.2} + + '@parcel/plugin@2.13.2': + resolution: {integrity: sha512-Q+RIENS1B185yLPhrGdzBK1oJrZmh/RXrYMnzJs78Tog8SpihjeNBNR6z4PT85o2F+Gy2y1S9A26fpiGq161qQ==} + engines: {node: '>= 16.0.0'} + + '@parcel/profiler@2.13.2': + resolution: {integrity: sha512-fur6Oq2HkX6AiM8rtqmDvldH5JWz0sqXA1ylz8cE3XOiDZIuvCulZmQ+hH+4odaNH6QocI1MwfV+GDh3HlQoCA==} + engines: {node: '>= 16.0.0'} + + '@parcel/reporter-cli@2.13.2': + resolution: {integrity: sha512-dIx4d/B+P+7n+lPCnjorM3ygHf3E/P3os3g6BjUe7gOlq/acTwtM0TNXNdRLcsw3K+RzA2VkHLnvdgjIJ18F5g==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/reporter-dev-server@2.13.2': + resolution: {integrity: sha512-alWCPZiXEy5a1/mVnxQTJwJhWrnjaR+JOHQSu69eBGuWHqhXt2SCyKpczT08nm37GIxkhsiIaVR8sP4lVriApw==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/reporter-tracer@2.13.2': + resolution: {integrity: sha512-QdnyUHrYcb5iIMqqp6nmR0xi63sPLTALsRYMoLpQPXP/SrO4JQIqGeBSdHi+59esDnlbWDtN2RpBJ3cXlOsjsA==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/resolver-default@2.13.2': + resolution: {integrity: sha512-8bMK04AxUmLF0+rsEl9u2LiboAsTjAemer9N/qMnWfsbxvEDunfTR39fwEEXpGAQV2sFb0ZPYtTxOc8bk5ygcQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/runtime-browser-hmr@2.13.2': + resolution: {integrity: sha512-ByF8Ww1g6XbtwqBxNZrUz/j9EG0O7sqefkW7E2IWhlxFiNJakIrgaN5VKCBRRWaDvyAz0Kn6Md9e6GLmioRXkA==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/runtime-js@2.13.2': + resolution: {integrity: sha512-DxRFW30RWM8noK1+yrqa+GYblMJabx6cg5Q7BI1SmTvVflomYVy2KEBVA161VZoxjHS6o0lToziAeVcUJT5GUQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/runtime-react-refresh@2.13.2': + resolution: {integrity: sha512-anLQUANkU++brMa7PWBmvbGDgaNMA9BP7vg/g22KI7w6nh9D3f4JBi/Vo4N66zHATpex41gAjGmFXcBtotc5bQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/runtime-service-worker@2.13.2': + resolution: {integrity: sha512-EWn3eM5d81uL9+hXqAnuXo/6yq/7p1VEOKn83FEsbO4TAb6Pd25bJ0mPnWpewPcJBQUoPX3Wjx7VzVit7eeuYw==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/rust@2.13.2': + resolution: {integrity: sha512-XFIewSwxkrDYOnnSP/XZ1LDLdXTs7L9CjQUWtl46Vir5Pq/rinemwLJeKGIwKLHy7fhUZQjYxquH6fBL+AY8DA==} + engines: {node: '>= 16.0.0'} + + '@parcel/source-map@2.1.1': + resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==} + engines: {node: ^12.18.3 || >=14} + + '@parcel/transformer-babel@2.13.2': + resolution: {integrity: sha512-2cHXLQ2+jeae+mImoaKTtkKhCKATaPY2+Pao0g3zh1xwhNu/08xj7upnbD548UEyEChUWn6IjWljDsx4y8Oa3w==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-css@2.13.2': + resolution: {integrity: sha512-QR9I4wYc+Tw7eET5ak3BvXLdsmDJGzq+Gd4KaULa0sNKioDUXCi79E5rGICW8E+BbHGKar7boNzk7HrNZX7PLg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-html@2.13.2': + resolution: {integrity: sha512-LlQHODz/R832ZuRkCGlOQe+TF1BR9nriUcVSc2Z7q5xQ/HblNPrVvvLDBcXG7xRToawS1y6jXG0Tihv47AykfQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-image@2.13.2': + resolution: {integrity: sha512-sHk9UmJIPEGil+8ulK+Mi4BArbSuMPTXrY1z3EP4pKGHPCMABNKIRiricngvxCW1eVZrxSokeHQe2jYWJ4tAtA==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/transformer-js@2.13.2': + resolution: {integrity: sha512-mn5DL+59x0FHeHKWOstZuKcz4rcVnZUAkXMPtERgXa0ggjJ1CgVOc26VD68sszC/aiF6yathz/LJtJpyluniLQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@parcel/transformer-json@2.13.2': + resolution: {integrity: sha512-AiLyWPnHaNvO9sGykYF15S3jzyQY0vSw3xQPj/xhDRv7IXQyt3y1zTtJmQsp/ri9vIzf2CruD42UXiaSPpbA8A==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-postcss@2.13.2': + resolution: {integrity: sha512-srcKQcTaaCGutcvpWeTue4/bScPJK3nXyql2QVNneufqxTQsOZcZg8lFaMc3ma6WjQn/m2emQC26eivr3MOhDg==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-posthtml@2.13.2': + resolution: {integrity: sha512-pNvxKp7GWLKSbyV2xRaGWZNV/ut8uetMfbwpcGxboxgq5TV9dqnHxRGzsTvZTo7yHqQ3N6hycoGh+w8L/8sg8Q==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-raw@2.13.2': + resolution: {integrity: sha512-KsTasFp+jwkGjBLrHO6oiqIIwOeiyNPx5NawmIzXUuGvQv6UhTSayk3NnFxteOVXzy5C9GfrQ5W+IBrHe6JWaw==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-react-refresh-wrap@2.13.2': + resolution: {integrity: sha512-2UuuzHzpUx8Z0muoM3cETa7PDRJIG9a5nxPaTBZttT5ucprskITakky5pzsd4gabmNzWfZ5raRG5ixV3zOSL5A==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-sass@2.13.2': + resolution: {integrity: sha512-FemdyKa6wvkitG2DQgkDI6NkyJCsQ2My/z3idcFAyf8kb3KBIJ+a0ZK4QALvLnJiC9ugeIKsZk5uFjoJAHX1XQ==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/transformer-svg@2.13.2': + resolution: {integrity: sha512-ANwWE4/n4rXrlbmY3iT18ndlxlLP1ubapR1wYL9bpp2cKA8ny2tCe5wlzLxBAfwcZx8cd15N/5v/ZwS6xt6BXw==} + engines: {node: '>= 16.0.0', parcel: ^2.13.2} + + '@parcel/types-internal@2.13.2': + resolution: {integrity: sha512-j0zb3WNM8O/+d8CArll7/4w4AyBED3Jbo32/unz89EPVN0VklmgBrRCAI5QXDKuJAGdAZSL5/a8bNYbwl7/Wxw==} + + '@parcel/types@2.13.2': + resolution: {integrity: sha512-6ixqjk2pjKELn4sQ/jdvpbCVTeH6xXQTdotkN8Wzk68F2K2MtSPIRAEocumlexScfffbRQplr2MdIf1JJWLogA==} + + '@parcel/utils@2.13.2': + resolution: {integrity: sha512-BkFtRo5xenmonwnBy+X4sVbHIRrx+ZHMPpS/6hFqyTvoUUFq2yTFQnfRGVVOOvscVUxpGom+kewnrTG3HHbZoA==} + engines: {node: '>= 16.0.0'} + + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + + '@parcel/workers@2.13.2': + resolution: {integrity: sha512-P78BpH0yTT9KK09wgK4eabtlb5OlcWAmZebOToN5UYuwWEylKt0gWZx1+d+LPQupvK84/iZ+AutDScsATjgUMw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@parcel/core': ^2.13.2 + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rc-component/async-validator@5.0.4': + resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} + engines: {node: '>=14.x'} + + '@rc-component/color-picker@2.0.1': + resolution: {integrity: sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/context@1.4.0': + resolution: {integrity: sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mini-decimal@1.1.0': + resolution: {integrity: sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==} + engines: {node: '>=8.x'} + + '@rc-component/mutate-observer@1.1.0': + resolution: {integrity: sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/portal@1.1.2': + resolution: {integrity: sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/qrcode@1.0.0': + resolution: {integrity: sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tour@1.15.1': + resolution: {integrity: sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/trigger@2.2.5': + resolution: {integrity: sha512-F1EJ4KjFpGAHAjuKvOyZB/6IZDkVx0bHl0M4fQM5wXcmm7lgTgVSSnR3bXwdmS6jOJGHOqfDxIJW3WUvwMIXhQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@remix-run/router@1.21.0': + resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==} + engines: {node: '>=14.0.0'} + + '@swc/core-darwin-arm64@1.9.3': + resolution: {integrity: sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.9.3': + resolution: {integrity: sha512-IaRq05ZLdtgF5h9CzlcgaNHyg4VXuiStnOFpfNEMuI5fm5afP2S0FHq8WdakUz5WppsbddTdplL+vpeApt/WCQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.9.3': + resolution: {integrity: sha512-Pbwe7xYprj/nEnZrNBvZfjnTxlBIcfApAGdz2EROhjpPj+FBqBa3wOogqbsuGGBdCphf8S+KPprL1z+oDWkmSQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.9.3': + resolution: {integrity: sha512-AQ5JZiwNGVV/2K2TVulg0mw/3LYfqpjZO6jDPtR2evNbk9Yt57YsVzS+3vHSlUBQDRV9/jqMuZYVU3P13xrk+g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.9.3': + resolution: {integrity: sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.9.3': + resolution: {integrity: sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.9.3': + resolution: {integrity: sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.9.3': + resolution: {integrity: sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.9.3': + resolution: {integrity: sha512-rqpzNfpAooSL4UfQnHhkW8aL+oyjqJniDP0qwZfGnjDoJSbtPysHg2LpcOBEdSnEH+uIZq6J96qf0ZFD8AGfXA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.9.3': + resolution: {integrity: sha512-3YJJLQ5suIEHEKc1GHtqVq475guiyqisKSoUnoaRtxkDaW5g1yvPt9IoSLOe2mRs7+FFhGGU693RsBUSwOXSdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.9.3': + resolution: {integrity: sha512-oRj0AFePUhtatX+BscVhnzaAmWjpfAeySpM1TCbxA1rtBDeH/JDhi5yYzAKneDYtVtBvA7ApfeuzhMC9ye4xSg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} + + '@types/node@20.17.8': + resolution: {integrity: sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==} + + '@types/prop-types@15.7.13': + resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} + + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + + '@types/react-virtualized@9.22.0': + resolution: {integrity: sha512-JL/YCCFZ123za//cj10Apk54F0UGFMrjOE0QHTuXt1KBMFrzLOGv9/x6Uc/pZ0Gaf4o6w61Fostvlw0DwuPXig==} + + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + antd@5.22.2: + resolution: {integrity: sha512-vihhiJbm9VG3d6boUeD1q2MXMax+qBrXhgqCEC+45v8iGUF6m4Ct+lFiCW4oWaN3EABOsbVA6Svy3Rj/QkQFKw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-x@3.0.10: + resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001684: + resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.65: + resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + get-port@4.2.0: + resolution: {integrity: sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==} + engines: {node: '>=6'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + htmlnano@2.1.1: + resolution: {integrity: sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==} + peerDependencies: + cssnano: ^7.0.0 + postcss: ^8.3.11 + purgecss: ^6.0.0 + relateurl: ^0.2.7 + srcset: 5.0.1 + svgo: ^3.0.2 + terser: ^5.10.0 + uncss: ^0.17.3 + peerDependenciesMeta: + cssnano: + optional: true + postcss: + optional: true + purgecss: + optional: true + relateurl: + optional: true + srcset: + optional: true + svgo: + optional: true + terser: + optional: true + uncss: + optional: true + + htmlparser2@7.2.0: + resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-json@2.0.1: + resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-darwin-arm64@1.28.2: + resolution: {integrity: sha512-/8cPSqZiusHSS+WQz0W4NuaqFjquys1x+NsdN/XOHb+idGHJSoJ7SoQTVl3DZuAgtPZwFZgRfb/vd1oi8uX6+g==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.28.2: + resolution: {integrity: sha512-R7sFrXlgKjvoEG8umpVt/yutjxOL0z8KWf0bfPT3cYMOW4470xu5qSHpFdIOpRWwl3FKNMUdbKtMUjYt0h2j4g==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.28.2: + resolution: {integrity: sha512-l2qrCT+x7crAY+lMIxtgvV10R8VurzHAoUZJaVFSlHrN8kRLTvEg9ObojIDIexqWJQvJcVVV3vfzsEynpiuvgA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.28.2: + resolution: {integrity: sha512-DKMzpICBEKnL53X14rF7hFDu8KKALUJtcKdFUCW5YOlGSiwRSgVoRjM97wUm/E0NMPkzrTi/rxfvt7ruNK8meg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.28.2: + resolution: {integrity: sha512-nhfjYkfymWZSxdtTNMWyhFk2ImUm0X7NAgJWFwnsYPOfmtWQEapzG/DXZTfEfMjSzERNUNJoQjPAbdqgB+sjiw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.28.2: + resolution: {integrity: sha512-1SPG1ZTNnphWvAv8RVOymlZ8BDtAg69Hbo7n4QxARvkFVCJAt0cgjAw1Fox0WEhf4PwnyoOBaVH0Z5YNgzt4dA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.28.2: + resolution: {integrity: sha512-ZhQy0FcO//INWUdo/iEdbefntTdpPVQ0XJwwtdbBuMQe+uxqZoytm9M+iqR9O5noWFaxK+nbS2iR/I80Q2Ofpg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.28.2: + resolution: {integrity: sha512-alb/j1NMrgQmSFyzTbN1/pvMPM+gdDw7YBuQ5VSgcFDypN3Ah0BzC2dTZbzwzaMdUVDszX6zH5MzjfVN1oGuww==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.28.2: + resolution: {integrity: sha512-WnwcjcBeAt0jGdjlgbT9ANf30pF0C/QMb1XnLnH272DQU8QXh+kmpi24R55wmWBwaTtNAETZ+m35ohyeMiNt+g==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.28.2: + resolution: {integrity: sha512-3piBifyT3avz22o6mDKywQC/OisH2yDK+caHWkiMsF82i3m5wDBadyCjlCQ5VNgzYkxrWZgiaxHDdd5uxsi0/A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.28.2: + resolution: {integrity: sha512-ePLRrbt3fgjXI5VFZOLbvkLD5ZRuxGKm+wJ3ujCqBtL3NanDHPo/5zicR5uEKAPiIjBYF99BM4K4okvMznjkVA==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lmdb@2.8.5: + resolution: {integrity: sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==} + hasBin: true + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.2: + resolution: {integrity: sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + ordered-binary@1.5.3: + resolution: {integrity: sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parcel@2.13.2: + resolution: {integrity: sha512-ROp1Lf6cihWYzdkieXH+KWVkjlqiUMqW18MBMNZQ3sQitnXWGozTgSYIfpUFLQqaHLgBfm5inOwdqmbzExdpYA==} + engines: {node: '>= 16.0.0'} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + posthtml-parser@0.11.0: + resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==} + engines: {node: '>=12'} + + posthtml-parser@0.12.1: + resolution: {integrity: sha512-rYFmsDLfYm+4Ts2Oh4DCDSZPtdC1BLnRXAobypVzX9alj28KGl65dIFtgDY9zB57D0TC4Qxqrawuq/2et1P0GA==} + engines: {node: '>=16'} + + posthtml-render@3.0.0: + resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==} + engines: {node: '>=12'} + + posthtml@0.16.6: + resolution: {integrity: sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==} + engines: {node: '>=12.0.0'} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + rc-cascader@3.30.0: + resolution: {integrity: sha512-rrzSbk1Bdqbu+pDwiLCLHu72+lwX9BZ28+JKzoi0DWZ4N29QYFeip8Gctl33QVd2Xg3Rf14D3yAOG76ElJw16w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-checkbox@3.3.0: + resolution: {integrity: sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-collapse@3.9.0: + resolution: {integrity: sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dialog@9.6.0: + resolution: {integrity: sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-drawer@7.2.0: + resolution: {integrity: sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dropdown@4.2.0: + resolution: {integrity: sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==} + peerDependencies: + react: '>=16.11.0' + react-dom: '>=16.11.0' + + rc-field-form@2.5.1: + resolution: {integrity: sha512-33hunXwynQJyeae7LS3hMGTXNeRBjiPyPYgB0824EbmLHiXC1EBGyUwRh6xjLRy9c+en5WARYN0gJz5+JAqwig==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-image@7.11.0: + resolution: {integrity: sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-input-number@9.3.0: + resolution: {integrity: sha512-JQ363ywqRyxwgVxpg2z2kja3CehTpYdqR7emJ/6yJjRdbvo+RvfE83fcpBCIJRq3zLp8SakmEXq60qzWyZ7Usw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-input@1.6.3: + resolution: {integrity: sha512-wI4NzuqBS8vvKr8cljsvnTUqItMfG1QbJoxovCgL+DX4eVUcHIjVwharwevIxyy7H/jbLryh+K7ysnJr23aWIA==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + rc-mentions@2.17.0: + resolution: {integrity: sha512-sfHy+qLvc+p8jx8GUsujZWXDOIlIimp6YQz7N5ONQ6bHsa2kyG+BLa5k2wuxgebBbH97is33wxiyq5UkiXRpHA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-menu@9.16.0: + resolution: {integrity: sha512-vAL0yqPkmXWk3+YKRkmIR8TYj3RVdEt3ptG2jCJXWNAvQbT0VJJdRyHZ7kG/l1JsZlB+VJq/VcYOo69VR4oD+w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-motion@2.9.3: + resolution: {integrity: sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-notification@5.6.2: + resolution: {integrity: sha512-Id4IYMoii3zzrG0lB0gD6dPgJx4Iu95Xu0BQrhHIbp7ZnAZbLqdqQ73aIWH0d0UFcElxwaKjnzNovTjo7kXz7g==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-overflow@1.3.2: + resolution: {integrity: sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-pagination@4.3.0: + resolution: {integrity: sha512-UubEWA0ShnroQ1tDa291Fzw6kj0iOeF26IsUObxYTpimgj4/qPCWVFl18RLZE+0Up1IZg0IK4pMn6nB3mjvB7g==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-picker@4.8.2: + resolution: {integrity: sha512-I6Nn4ngkRskSD//rsXDvjlEQ8CzX9kPQrUIb7+qTY49erJaa3/oKJWmi6JIxo/A7gy59phNmPTdhKosAa/NrQQ==} + engines: {node: '>=8.x'} + peerDependencies: + date-fns: '>= 2.x' + dayjs: '>= 1.x' + luxon: '>= 3.x' + moment: '>= 2.x' + react: '>=16.9.0' + react-dom: '>=16.9.0' + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + + rc-progress@4.0.0: + resolution: {integrity: sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-rate@2.13.0: + resolution: {integrity: sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-resize-observer@1.4.0: + resolution: {integrity: sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-segmented@2.5.0: + resolution: {integrity: sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + rc-select@14.16.3: + resolution: {integrity: sha512-51+j6s3fJJJXB7E+B6W1hM4Tjzv1B/Decooz9ilgegDBt3ZAth1b/xMwYCTrT5BbG2e53XACQsyDib2+3Ro1fg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-slider@11.1.7: + resolution: {integrity: sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-steps@6.0.1: + resolution: {integrity: sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-switch@4.1.0: + resolution: {integrity: sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-table@7.48.1: + resolution: {integrity: sha512-Z4mDKjWg+xz/Ezdw6ivWcbqRpaJ0QfCORRoRrlrw65KSGZLK8OcTdacH22/fyGb8L4It/0/9qcMm8VrVAk/WBw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tabs@15.4.0: + resolution: {integrity: sha512-llKuyiAVqmXm2z7OrmhX5cNb2ueZaL8ZyA2P4R+6/72NYYcbEgOXibwHiQCFY2RiN3swXl53SIABi2CumUS02g==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-textarea@1.8.2: + resolution: {integrity: sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tooltip@6.2.1: + resolution: {integrity: sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tree-select@5.24.5: + resolution: {integrity: sha512-PnyR8LZJWaiEFw0SHRqo4MNQWyyZsyMs8eNmo68uXZWjxc7QqeWcjPPoONN0rc90c3HZqGF9z+Roz+GLzY5GXA==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-tree@5.10.1: + resolution: {integrity: sha512-FPXb3tT/u39mgjr6JNlHaUTYfHkVGW56XaGDahDpEFLGsnPxGcVLNTjcqoQb/GNbSCycl7tD7EvIymwOTP0+Yw==} + engines: {node: '>=10.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-upload@4.8.1: + resolution: {integrity: sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-util@5.43.0: + resolution: {integrity: sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-virtual-list@3.15.0: + resolution: {integrity: sha512-dF2YQztqrU3ijAeWOqscTshCEr7vpimzSqAVjO1AyAmaqcHulaXpnGR0ptK5PXfxTUy48VkJOiglMIxlkYGs0w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-error-overlay@6.0.9: + resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-js-cron@5.0.1: + resolution: {integrity: sha512-qHUb/qWeMvXklGW6/hLtH9CjboRU7pu2hHxGGErUDTjHDOLn/b6CZHvVsx/e3ak+UObwf4Y0BHSQJzpn1JpvvA==} + peerDependencies: + antd: '>=5.8.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.28.0: + resolution: {integrity: sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.28.0: + resolution: {integrity: sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react-smooth@4.0.1: + resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-virtualized@9.22.5: + resolution: {integrity: sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.13.3: + resolution: {integrity: sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sass@1.81.0: + resolution: {integrity: sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + stylis@4.3.4: + resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} + engines: {node: '>=12.22'} + + timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + weak-lru-cache@1.2.2: + resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@ant-design/colors@7.1.0': + dependencies: + '@ctrl/tinycolor': 3.6.1 + + '@ant-design/cssinjs-utils@1.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ant-design/cssinjs': 1.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@babel/runtime': 7.26.0 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@ant-design/cssinjs@1.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + classnames: 2.5.1 + csstype: 3.1.3 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + stylis: 4.3.4 + + '@ant-design/fast-color@2.0.6': + dependencies: + '@babel/runtime': 7.26.0 + + '@ant-design/icons-svg@4.4.2': {} + + '@ant-design/icons@5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ant-design/colors': 7.1.0 + '@ant-design/icons-svg': 4.4.2 + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@ant-design/react-slick@1.1.2(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + json2mq: 0.2.0 + react: 18.3.1 + resize-observer-polyfill: 1.5.1 + throttle-debounce: 5.0.2 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@bufbuild/protobuf@2.2.2': {} + + '@connectrpc/connect-web@2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2))': + dependencies: + '@bufbuild/protobuf': 2.2.2 + '@connectrpc/connect': 2.0.0(@bufbuild/protobuf@2.2.2) + + '@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2)': + dependencies: + '@bufbuild/protobuf': 2.2.2 + + '@ctrl/tinycolor@3.6.1': {} + + '@emotion/hash@0.8.0': {} + + '@emotion/unitless@0.7.5': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@lezer/common@1.2.3': {} + + '@lezer/lr@1.4.2': + dependencies: + '@lezer/common': 1.2.3 + + '@lmdb/lmdb-darwin-arm64@2.8.5': + optional: true + + '@lmdb/lmdb-darwin-x64@2.8.5': + optional: true + + '@lmdb/lmdb-linux-arm64@2.8.5': + optional: true + + '@lmdb/lmdb-linux-arm@2.8.5': + optional: true + + '@lmdb/lmdb-linux-x64@2.8.5': + optional: true + + '@lmdb/lmdb-win32-x64@2.8.5': + optional: true + + '@mischnic/json-sourcemap@0.1.1': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/lr': 1.4.2 + json5: 2.2.3 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@parcel/bundler-default@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/graph': 3.3.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/cache@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/fs': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/logger': 2.13.2 + '@parcel/utils': 2.13.2 + lmdb: 2.8.5 + + '@parcel/codeframe@2.13.2': + dependencies: + chalk: 4.1.2 + + '@parcel/compressor-raw@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/config-default@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(svgo@3.3.2)(typescript@5.7.2)': + dependencies: + '@parcel/bundler-default': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/compressor-raw': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/namer-default': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/optimizer-css': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/optimizer-htmlnano': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(svgo@3.3.2)(typescript@5.7.2) + '@parcel/optimizer-image': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/optimizer-svgo': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/optimizer-swc': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15) + '@parcel/packager-css': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/packager-html': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/packager-js': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/packager-raw': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/packager-svg': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/packager-wasm': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/reporter-dev-server': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/resolver-default': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/runtime-browser-hmr': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/runtime-js': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/runtime-react-refresh': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/runtime-service-worker': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-babel': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-css': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-html': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-image': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-js': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-json': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-postcss': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-posthtml': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-raw': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-react-refresh-wrap': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/transformer-svg': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@swc/helpers' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - svgo + - terser + - typescript + - uncss + + '@parcel/core@2.13.2(@swc/helpers@0.5.15)': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + '@parcel/cache': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/diagnostic': 2.13.2 + '@parcel/events': 2.13.2 + '@parcel/feature-flags': 2.13.2 + '@parcel/fs': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/graph': 3.3.2 + '@parcel/logger': 2.13.2 + '@parcel/package-manager': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15) + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/profiler': 2.13.2 + '@parcel/rust': 2.13.2 + '@parcel/source-map': 2.1.1 + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + base-x: 3.0.10 + browserslist: 4.24.2 + clone: 2.1.2 + dotenv: 16.4.5 + dotenv-expand: 11.0.7 + json5: 2.2.3 + msgpackr: 1.11.2 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/diagnostic@2.13.2': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + nullthrows: 1.1.1 + + '@parcel/events@2.13.2': {} + + '@parcel/feature-flags@2.13.2': {} + + '@parcel/fs@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/feature-flags': 2.13.2 + '@parcel/rust': 2.13.2 + '@parcel/types-internal': 2.13.2 + '@parcel/utils': 2.13.2 + '@parcel/watcher': 2.5.0 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + + '@parcel/graph@3.3.2': + dependencies: + '@parcel/feature-flags': 2.13.2 + nullthrows: 1.1.1 + + '@parcel/logger@2.13.2': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/events': 2.13.2 + + '@parcel/markdown-ansi@2.13.2': + dependencies: + chalk: 4.1.2 + + '@parcel/namer-default@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/node-resolver-core@3.4.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + '@parcel/diagnostic': 2.13.2 + '@parcel/fs': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-css@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + browserslist: 4.24.2 + lightningcss: 1.28.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-htmlnano@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(svgo@3.3.2)(typescript@5.7.2)': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + htmlnano: 2.1.1(svgo@3.3.2)(typescript@5.7.2) + nullthrows: 1.1.1 + posthtml: 0.16.6 + transitivePeerDependencies: + - '@parcel/core' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - svgo + - terser + - typescript + - uncss + + '@parcel/optimizer-image@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/utils': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + + '@parcel/optimizer-svgo@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-swc@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + '@swc/core': 1.9.3(@swc/helpers@0.5.15) + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + - '@swc/helpers' + + '@parcel/package-manager@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/diagnostic': 2.13.2 + '@parcel/fs': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/logger': 2.13.2 + '@parcel/node-resolver-core': 3.4.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@swc/core': 1.9.3(@swc/helpers@0.5.15) + semver: 7.6.3 + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/packager-css@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + lightningcss: 1.28.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-html@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + posthtml: 0.16.6 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-js@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/source-map': 2.1.1 + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + globals: 13.24.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-raw@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-svg@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + posthtml: 0.16.6 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-wasm@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/plugin@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/profiler@2.13.2': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/events': 2.13.2 + '@parcel/types-internal': 2.13.2 + chrome-trace-event: 1.0.4 + + '@parcel/reporter-cli@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/types': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + chalk: 4.1.2 + term-size: 2.2.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/reporter-dev-server@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/reporter-tracer@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + chrome-trace-event: 1.0.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/resolver-default@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/node-resolver-core': 3.4.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-browser-hmr@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-js@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-react-refresh@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + react-error-overlay: 6.0.9 + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-service-worker@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/rust@2.13.2': {} + + '@parcel/source-map@2.1.1': + dependencies: + detect-libc: 1.0.3 + + '@parcel/transformer-babel@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + browserslist: 4.24.2 + json5: 2.2.3 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-css@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + browserslist: 4.24.2 + lightningcss: 1.28.2 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-html@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.12.1 + posthtml-render: 3.0.0 + semver: 7.6.3 + srcset: 4.0.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-image@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + nullthrows: 1.1.1 + + '@parcel/transformer-js@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@swc/helpers': 0.5.15 + browserslist: 4.24.2 + nullthrows: 1.1.1 + regenerator-runtime: 0.14.1 + semver: 7.6.3 + + '@parcel/transformer-json@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + json5: 2.2.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-postcss@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + '@parcel/utils': 2.13.2 + clone: 2.1.2 + nullthrows: 1.1.1 + postcss-value-parser: 4.2.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-posthtml@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.12.1 + posthtml-render: 3.0.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-raw@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-react-refresh-wrap@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-sass@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/source-map': 2.1.1 + sass: 1.81.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-svg@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/plugin': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/rust': 2.13.2 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.12.1 + posthtml-render: 3.0.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/types-internal@2.13.2': + dependencies: + '@parcel/diagnostic': 2.13.2 + '@parcel/feature-flags': 2.13.2 + '@parcel/source-map': 2.1.1 + utility-types: 3.11.0 + + '@parcel/types@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/types-internal': 2.13.2 + '@parcel/workers': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/utils@2.13.2': + dependencies: + '@parcel/codeframe': 2.13.2 + '@parcel/diagnostic': 2.13.2 + '@parcel/logger': 2.13.2 + '@parcel/markdown-ansi': 2.13.2 + '@parcel/rust': 2.13.2 + '@parcel/source-map': 2.1.1 + chalk: 4.1.2 + nullthrows: 1.1.1 + + '@parcel/watcher-android-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-x64@2.5.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-win32-arm64@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + + '@parcel/workers@2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))': + dependencies: + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/diagnostic': 2.13.2 + '@parcel/logger': 2.13.2 + '@parcel/profiler': 2.13.2 + '@parcel/types-internal': 2.13.2 + '@parcel/utils': 2.13.2 + nullthrows: 1.1.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rc-component/async-validator@5.0.4': + dependencies: + '@babel/runtime': 7.26.0 + + '@rc-component/color-picker@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ant-design/fast-color': 2.0.6 + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/context@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/mini-decimal@1.1.0': + dependencies: + '@babel/runtime': 7.26.0 + + '@rc-component/mutate-observer@1.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/portal@1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/qrcode@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/tour@1.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/trigger@2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@remix-run/router@1.21.0': {} + + '@swc/core-darwin-arm64@1.9.3': + optional: true + + '@swc/core-darwin-x64@1.9.3': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.9.3': + optional: true + + '@swc/core-linux-arm64-gnu@1.9.3': + optional: true + + '@swc/core-linux-arm64-musl@1.9.3': + optional: true + + '@swc/core-linux-x64-gnu@1.9.3': + optional: true + + '@swc/core-linux-x64-musl@1.9.3': + optional: true + + '@swc/core-win32-arm64-msvc@1.9.3': + optional: true + + '@swc/core-win32-ia32-msvc@1.9.3': + optional: true + + '@swc/core-win32-x64-msvc@1.9.3': + optional: true + + '@swc/core@1.9.3(@swc/helpers@0.5.15)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.17 + optionalDependencies: + '@swc/core-darwin-arm64': 1.9.3 + '@swc/core-darwin-x64': 1.9.3 + '@swc/core-linux-arm-gnueabihf': 1.9.3 + '@swc/core-linux-arm64-gnu': 1.9.3 + '@swc/core-linux-arm64-musl': 1.9.3 + '@swc/core-linux-x64-gnu': 1.9.3 + '@swc/core-linux-x64-musl': 1.9.3 + '@swc/core-win32-arm64-msvc': 1.9.3 + '@swc/core-win32-ia32-msvc': 1.9.3 + '@swc/core-win32-x64-msvc': 1.9.3 + '@swc/helpers': 0.5.15 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@swc/types@0.1.17': + dependencies: + '@swc/counter': 0.1.3 + + '@trysound/sax@0.2.0': {} + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/lodash@4.17.13': {} + + '@types/node@20.17.8': + dependencies: + undici-types: 6.19.8 + + '@types/prop-types@15.7.13': {} + + '@types/react-dom@18.3.1': + dependencies: + '@types/react': 18.3.12 + + '@types/react-virtualized@9.22.0': + dependencies: + '@types/prop-types': 15.7.13 + '@types/react': 18.3.12 + + '@types/react@18.3.12': + dependencies: + '@types/prop-types': 15.7.13 + csstype: 3.1.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + antd@5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@ant-design/colors': 7.1.0 + '@ant-design/cssinjs': 1.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/cssinjs-utils': 1.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/react-slick': 1.1.2(react@18.3.1) + '@babel/runtime': 7.26.0 + '@ctrl/tinycolor': 3.6.1 + '@rc-component/color-picker': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rc-component/mutate-observer': 1.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rc-component/qrcode': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rc-component/tour': 1.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + copy-to-clipboard: 3.3.3 + dayjs: 1.11.13 + rc-cascader: 3.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-checkbox: 3.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-collapse: 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-dialog: 9.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-drawer: 7.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-dropdown: 4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-field-form: 2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-image: 7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-input-number: 9.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-mentions: 2.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-menu: 9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-notification: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-pagination: 4.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-picker: 4.8.2(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-progress: 4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-rate: 2.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-segmented: 2.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-select: 14.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-slider: 11.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-steps: 6.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-switch: 4.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-table: 7.48.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tabs: 15.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-textarea: 1.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tooltip: 6.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tree: 5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tree-select: 5.24.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-upload: 4.8.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + scroll-into-view-if-needed: 3.1.0 + throttle-debounce: 5.0.2 + transitivePeerDependencies: + - date-fns + - luxon + - moment + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + base-x@3.0.10: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + boolbase@1.0.0: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001684 + electron-to-chromium: 1.5.65 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001684: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + + chrome-trace-event@1.0.4: {} + + classnames@2.5.1: {} + + clone@2.1.2: {} + + clsx@1.2.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@7.2.0: {} + + compute-scroll-into-view@3.1.0: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + cosmiconfig@9.0.0(typescript@5.7.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.7.2 + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + dayjs@1.11.13: {} + + decimal.js-light@2.5.1: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.0 + csstype: 3.1.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.4.5 + + dotenv@16.4.5: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.65: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@2.2.0: {} + + entities@3.0.1: {} + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.2.0: {} + + eventemitter3@4.0.7: {} + + events@3.3.0: {} + + fast-equals@5.0.1: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + get-port@4.2.0: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + has-flag@4.0.0: {} + + htmlnano@2.1.1(svgo@3.3.2)(typescript@5.7.2): + dependencies: + cosmiconfig: 9.0.0(typescript@5.7.2) + posthtml: 0.16.6 + timsort: 0.3.0 + optionalDependencies: + svgo: 3.3.2 + transitivePeerDependencies: + - typescript + + htmlparser2@7.2.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 3.0.1 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + ieee754@1.2.1: {} + + immutable@5.0.3: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + internmap@2.0.3: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-json@2.0.1: {} + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + + json5@2.2.3: {} + + lightningcss-darwin-arm64@1.28.2: + optional: true + + lightningcss-darwin-x64@1.28.2: + optional: true + + lightningcss-freebsd-x64@1.28.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.28.2: + optional: true + + lightningcss-linux-arm64-gnu@1.28.2: + optional: true + + lightningcss-linux-arm64-musl@1.28.2: + optional: true + + lightningcss-linux-x64-gnu@1.28.2: + optional: true + + lightningcss-linux-x64-musl@1.28.2: + optional: true + + lightningcss-win32-arm64-msvc@1.28.2: + optional: true + + lightningcss-win32-x64-msvc@1.28.2: + optional: true + + lightningcss@1.28.2: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.28.2 + lightningcss-darwin-x64: 1.28.2 + lightningcss-freebsd-x64: 1.28.2 + lightningcss-linux-arm-gnueabihf: 1.28.2 + lightningcss-linux-arm64-gnu: 1.28.2 + lightningcss-linux-arm64-musl: 1.28.2 + lightningcss-linux-x64-gnu: 1.28.2 + lightningcss-linux-x64-musl: 1.28.2 + lightningcss-win32-arm64-msvc: 1.28.2 + lightningcss-win32-x64-msvc: 1.28.2 + + lines-and-columns@1.2.4: {} + + lmdb@2.8.5: + dependencies: + msgpackr: 1.11.2 + node-addon-api: 6.1.0 + node-gyp-build-optional-packages: 5.1.1 + ordered-binary: 1.5.3 + weak-lru-cache: 1.2.2 + optionalDependencies: + '@lmdb/lmdb-darwin-arm64': 2.8.5 + '@lmdb/lmdb-darwin-x64': 2.8.5 + '@lmdb/lmdb-linux-arm': 2.8.5 + '@lmdb/lmdb-linux-arm64': 2.8.5 + '@lmdb/lmdb-linux-x64': 2.8.5 + '@lmdb/lmdb-win32-x64': 2.8.5 + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.2: + optionalDependencies: + msgpackr-extract: 3.0.3 + + node-addon-api@6.1.0: {} + + node-addon-api@7.1.1: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.3 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.3 + optional: true + + node-releases@2.0.18: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nullthrows@1.1.1: {} + + object-assign@4.1.1: {} + + ordered-binary@1.5.3: {} + + package-json-from-dist@1.0.1: {} + + parcel@2.13.2(@swc/helpers@0.5.15)(svgo@3.3.2)(typescript@5.7.2): + dependencies: + '@parcel/config-default': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(svgo@3.3.2)(typescript@5.7.2) + '@parcel/core': 2.13.2(@swc/helpers@0.5.15) + '@parcel/diagnostic': 2.13.2 + '@parcel/events': 2.13.2 + '@parcel/feature-flags': 2.13.2 + '@parcel/fs': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/logger': 2.13.2 + '@parcel/package-manager': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15))(@swc/helpers@0.5.15) + '@parcel/reporter-cli': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/reporter-dev-server': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/reporter-tracer': 2.13.2(@parcel/core@2.13.2(@swc/helpers@0.5.15)) + '@parcel/utils': 2.13.2 + chalk: 4.1.2 + commander: 12.1.0 + get-port: 4.2.0 + transitivePeerDependencies: + - '@swc/helpers' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - svgo + - terser + - typescript + - uncss + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + postcss-value-parser@4.2.0: {} + + posthtml-parser@0.11.0: + dependencies: + htmlparser2: 7.2.0 + + posthtml-parser@0.12.1: + dependencies: + htmlparser2: 9.1.0 + + posthtml-render@3.0.0: + dependencies: + is-json: 2.0.1 + + posthtml@0.16.6: + dependencies: + posthtml-parser: 0.11.0 + posthtml-render: 3.0.0 + + process@0.11.10: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + rc-cascader@3.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-select: 14.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tree: 5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-checkbox@3.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-collapse@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-dialog@9.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-drawer@7.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-dropdown@4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-field-form@2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/async-validator': 5.0.4 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-image@7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-dialog: 9.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-input-number@9.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/mini-decimal': 1.1.0 + classnames: 2.5.1 + rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-input@1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-mentions@2.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-menu: 9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-textarea: 1.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-menu@9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-overflow: 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-motion@2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-notification@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-overflow@1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-pagination@4.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-picker@4.8.2(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-overflow: 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + dayjs: 1.11.13 + + rc-progress@4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-rate@2.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-resize-observer@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resize-observer-polyfill: 1.5.1 + + rc-segmented@2.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-select@14.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-overflow: 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: 3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-slider@11.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-steps@6.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-switch@4.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-table@7.48.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/context': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: 3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-tabs@15.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-dropdown: 4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-menu: 9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-textarea@1.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-tooltip@6.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-tree-select@5.24.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-select: 14.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tree: 5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-tree@5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: 3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-upload@4.8.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-util@5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + + rc-virtual-list@3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-error-overlay@6.0.9: {} + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-js-cron@5.0.1(antd@5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + antd: 5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-lifecycles-compat@3.0.4: {} + + react-refresh@0.14.2: {} + + react-router-dom@6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.21.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.28.0(react@18.3.1) + + react-router@6.28.0(react@18.3.1): + dependencies: + '@remix-run/router': 1.21.0 + react: 18.3.1 + + react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-virtualized@9.22.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + clsx: 1.2.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readdirp@4.0.2: {} + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + regenerator-runtime@0.14.1: {} + + resize-observer-polyfill@1.5.1: {} + + resolve-from@4.0.0: {} + + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + safe-buffer@5.2.1: {} + + sass@1.81.0: + dependencies: + chokidar: 4.0.1 + immutable: 5.0.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.0 + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + srcset@4.0.0: {} + + string-convert@0.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + stylis@4.3.4: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + + term-size@2.2.1: {} + + throttle-debounce@5.0.2: {} + + timsort@0.3.0: {} + + tiny-invariant@1.3.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toggle-selection@1.0.6: {} + + tslib@2.8.1: {} + + type-fest@0.20.2: {} + + typescript@5.7.2: {} + + undici-types@6.19.8: {} + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + utility-types@3.11.0: {} + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + weak-lru-cache@1.2.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 diff --git a/webui/src/.prettierrc b/webui/src/.prettierrc new file mode 100644 index 000000000..222861c34 --- /dev/null +++ b/webui/src/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/webui/src/api.ts b/webui/src/api.ts new file mode 100644 index 000000000..7f24dfcc1 --- /dev/null +++ b/webui/src/api.ts @@ -0,0 +1,36 @@ +import { useMemo } from "react"; +import { createConnectTransport } from "@connectrpc/connect-web"; +import { createClient } from "@connectrpc/connect"; +import { Authentication } from "../gen/ts/v1/authentication_pb"; +import { Backrest } from "../gen/ts/v1/service_pb"; + +const tokenKey = "backrest-ui-authToken"; + +export const setAuthToken = (token: string) => { + localStorage.setItem(tokenKey, token); +}; + +const fetch = ( + input: RequestInfo | URL, + init?: RequestInit, +): Promise => { + const headers = new Headers(init?.headers); + let token = localStorage.getItem(tokenKey); + if (token && token !== "") { + headers.set("Authorization", "Bearer " + token); + } + init = { ...init, headers }; + return window.fetch(input, init); +}; + +const transport = createConnectTransport({ + baseUrl: "./", + useBinaryFormat: true, + fetch: fetch as typeof globalThis.fetch, +}); + +export const authenticationService = createClient( + Authentication, + transport, +); +export const backrestService = createClient(Backrest, transport); diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx new file mode 100644 index 000000000..faa76f663 --- /dev/null +++ b/webui/src/components/ActivityBar.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from "react"; +import { + subscribeToOperations, + unsubscribeFromOperations, +} from "../state/oplog"; +import { formatDuration } from "../lib/formatting"; +import { + Operation, + OperationEvent, + OperationEventType, + OperationStatus, +} from "../../gen/ts/v1/operations_pb"; +import { + displayTypeToString, + getTypeForDisplay, +} from "../state/flowdisplayaggregator"; + +export const ActivityBar = () => { + const [activeOperations, setActiveOperations] = useState([]); + const setRefresh = useState(0)[1]; + + useEffect(() => { + const callback = (event?: OperationEvent, err?: Error) => { + if (!event || !event.event) { + return; + } + + switch (event.event.case) { + case "createdOperations": + case "updatedOperations": + const ops = event.event.value.operations; + setActiveOperations((oldOps) => { + oldOps = oldOps.filter( + (op) => !ops.find((newOp) => newOp.id === op.id) + ); + const newOps = ops.filter( + (newOp) => newOp.status === OperationStatus.STATUS_INPROGRESS + ); + return [...oldOps, ...newOps]; + }); + break; + case "deletedOperations": + const opIDs = event.event.value.values; + setActiveOperations((ops) => + ops.filter((op) => !opIDs.includes(op.id)) + ); + break; + } + }; + + subscribeToOperations(callback); + + setInterval(() => { + setRefresh((r) => r + 1); + }, 500); + + return () => { + unsubscribeFromOperations(callback); + }; + }, []); + + return ( + + {activeOperations.map((op, idx) => { + const displayName = displayTypeToString(getTypeForDisplay(op)); + + return ( + + {displayName} in progress for plan {op.planId} to {op.repoId} for{" "} + {formatDuration(Date.now() - Number(op.unixTimeStartMs))} + + ); + })} + + ); +}; diff --git a/webui/src/components/Alerts.tsx b/webui/src/components/Alerts.tsx new file mode 100644 index 000000000..36960bfce --- /dev/null +++ b/webui/src/components/Alerts.tsx @@ -0,0 +1,43 @@ +import React, { useContext } from "react"; + +import { message } from "antd"; +import { MessageInstance } from "antd/es/message/interface"; + +const MessageContext = React.createContext(null); + +export const AlertContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [messageApi, contextHolder] = message.useMessage(); + + return ( + <> + {contextHolder} + + {children} + + + ); +}; + +export const useAlertApi = () => { + return useContext(MessageContext); +}; + +export const formatErrorAlert = (error: any, prefix?: string) => { + prefix = prefix ? prefix.trim() + " " : "Error: "; + const contents = (error.message || "" + error) as string; + if (contents.includes("\n")) { + return ( + <> + {prefix} +
+          {contents}
+        
+ + ); + } + return `${prefix}: ${contents}`; +}; diff --git a/webui/src/components/ConfigProvider.tsx b/webui/src/components/ConfigProvider.tsx new file mode 100644 index 000000000..85f0e6816 --- /dev/null +++ b/webui/src/components/ConfigProvider.tsx @@ -0,0 +1,26 @@ +import React, { useContext, useEffect, useState } from "react"; +import { Config, Repo } from "../../gen/ts/v1/config_pb"; + +type ConfigCtx = [Config | null, (config: Config) => void]; + +const ConfigContext = React.createContext([null, () => {}]); + +export const ConfigContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [config, setConfig] = useState(null); + return ( + <> + + {children} + + + ); +}; + +export const useConfig = (): ConfigCtx => { + const context = useContext(ConfigContext); + return context; +}; diff --git a/webui/src/components/HooksFormList.tsx b/webui/src/components/HooksFormList.tsx new file mode 100644 index 000000000..24d73b02f --- /dev/null +++ b/webui/src/components/HooksFormList.tsx @@ -0,0 +1,509 @@ +import React, { useState } from "react"; +import { + Hook_Condition, + Hook_ConditionSchema, + Hook_OnError, + Hook_OnErrorSchema, +} from "../../gen/ts/v1/config_pb"; +import { + Button, + Card, + Form, + FormListFieldData, + Input, + Popover, + Select, + Tooltip, +} from "antd"; +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { Rule } from "antd/es/form"; + +export interface HookFormData { + hooks: { + conditions: string[]; + }[]; +} + +export interface HookFields { + conditions: string[]; + actionCommand?: any; + actionGotify?: any; + actionDiscord?: any; + actionWebhook?: any; + actionSlack?: any; + actionShoutrrr?: any; + actionHealthchecks?: any; +} + +export const hooksListTooltipText = ( + <> + Hooks let you configure actions e.g. notifications and scripts that run in + response to the backup lifecycle. See{" "} + + the hook documentation + {" "} + for available options, or + + the cookbook + + for scripting examples. + +); + +/** + * HooksFormList is a UI component for editing a list of hooks that can apply either at the repo level or at the plan level. + */ +export const HooksFormList = () => { + const form = Form.useFormInstance(); + + return ( + + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => { + const hookData = form.getFieldValue([ + "hooks", + field.name, + ]) as HookFields; + + return ( + + Hook {index} {findHookTypeName(hookData)} + remove(field.name)} + style={{ + marginRight: "5px", + marginTop: "2px", + float: "right", + }} + /> + + } + size="small" + style={{ marginBottom: "5px" }} + > + + + + Shoutrrr is a multi-platform notification service,{" "} + + see docs + {" "} + to learn more about supported services + + } + > +
Shoutrrr URL
+ + } + /> +
+ Text Template: + + + + + ); + }, + }, + { + name: "Discord", + template: { + actionDiscord: { + webhookUrl: "", + template: "{{ .Summary }}", + }, + conditions: [], + }, + oneofKey: "actionDiscord", + component: ({ field }: { field: FormListFieldData }) => { + return ( + <> + + Discord Webhook} + /> + + Text Template: + + + + + ); + }, + }, + { + name: "Gotify", + template: { + actionGotify: { + baseUrl: "", + token: "", + template: "{{ .Summary }}", + titleTemplate: + "Backrest {{ .EventName .Event }} in plan {{ .Plan.Id }}", + priority: 5, + }, + conditions: [], + }, + oneofKey: "actionGotify", + component: ({ field }: { field: FormListFieldData }) => { + return ( + <> + + Gotify Base URL} + /> + + + Gotify Token} + /> + + + Title Template} + /> + + Text Template: + + + + + Slack Webhook} + /> + + Text Template: + + + + + ); + }, + }, + { + name: "Healthchecks", + template: { + actionHealthchecks: { + webhookUrl: "", + template: "{{ .Summary }}", + }, + conditions: [], + }, + oneofKey: "actionHealthchecks", + component: ({ field }: { field: FormListFieldData }) => { + return ( + <> + + Ping URL} /> + + Text Template: + + + + + ); + }, + }, +]; + +const findHookTypeName = (field: HookFields): string => { + if (!field) { + return "Unknown"; + } + for (const hookType of hookTypes) { + if (hookType.oneofKey in field) { + return hookType.name; + } + } + return "Unknown"; +}; + +const HookBuilder = ({ field }: { field: FormListFieldData }) => { + const form = Form.useFormInstance(); + const hookData = form.getFieldValue(["hooks", field.name]) as HookFields; + + if (!hookData) { + return

Unknown hook type

; + } + + for (const hookType of hookTypes) { + if (hookType.oneofKey in hookData) { + return hookType.component({ field }); + } + } + + return

Unknown hook type

; +}; + +const ItemOnErrorSelector = ({ field }: { field: FormListFieldData }) => { + return ( + <> + + What happens when the hook fails (only effective on start hooks e.g. + backup start, prune start, check start) +
    +
  • + IGNORE - the failure is ignored, subsequent hooks and the backup + operation will run as normal. +
  • +
  • + FATAL - stops the backup with an error status (triggers an error + notification). Skips running all subsequent hooks. +
  • +
  • + CANCEL - marks the backup as cancelled but does not trigger any + error notification. Skips running all subsequent hooks. +
  • +
+ + } + > + Error Behavior: +
+ + + + + + {/* Plan.repo */} + + + name="repo" + label="Repository" + validateTrigger={["onChange", "onBlur"]} + initialValue={template ? template.repo : ""} + rules={[ + { + required: true, + message: "Please select repository", + }, + ]} + > + + + remove(index)} + style={{ paddingLeft: "5px" }} + /> + + ))} + + + + + + )} +
+ + + {/* Plan.retention */} + + + {/* Plan.hooks */} + Hooks} + > + + + + + {() => ( + +
+                          {JSON.stringify(form.getFieldsValue(), null, 2)}
+                        
+ + ), + }, + ]} + /> + )} +
+ + + + ); +}; + +const RetentionPolicyView = () => { + const form = Form.useFormInstance(); + const retention = Form.useWatch("retention", { form, preserve: true }) as any; + + const determineMode = () => { + if (!retention) { + return "policyTimeBucketed"; + } else if (retention.policyKeepLastN) { + return "policyKeepLastN"; + } else if (retention.policyKeepAll) { + return "policyKeepAll"; + } else if (retention.policyTimeBucketed) { + return "policyTimeBucketed"; + } + }; + + const mode = determineMode(); + + let elem: React.ReactNode = null; + if (mode === "policyKeepAll") { + elem = ( + <> +

+ All backups are retained e.g. for append-only repos. Ensure that you + manually forget / prune backups elsewhere. Backrest will register + forgets performed externally on the next backup. +

+ + + ); + } else if (mode === "policyKeepLastN") { + elem = ( + + Count} + type="number" + /> + + ); + } else if (mode === "policyTimeBucketed") { + elem = ( + + + + Yearly} + type="number" + /> + + + Monthly} + type="number" + /> + + + Weekly} + type="number" + /> + + + + + Daily} + type="number" + /> + + + Hourly} + type="number" + /> + + + + ); + } + + return ( + <> + + + { + const selected = e.target.value; + if (selected === "policyKeepLastN") { + form.setFieldValue("retention", { policyKeepLastN: 30 }); + } else if (selected === "policyTimeBucketed") { + form.setFieldValue("retention", { + policyTimeBucketed: { + yearly: 0, + monthly: 3, + weekly: 4, + daily: 7, + hourly: 24, + }, + }); + } else { + form.setFieldValue("retention", { policyKeepAll: true }); + } + }} + > + + + By Count + + + + + By Time Period + + + + + None + + + + +
+ + {elem} + +
+ + ); +}; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx new file mode 100644 index 000000000..7afea7377 --- /dev/null +++ b/webui/src/views/AddRepoModal.tsx @@ -0,0 +1,865 @@ +import { + Form, + Modal, + Input, + Typography, + AutoComplete, + Tooltip, + Button, + Row, + Col, + Card, + InputNumber, + FormInstance, + Collapse, + Checkbox, + Select, + Space, +} from "antd"; +import React, { useEffect, useState } from "react"; +import { useShowModal } from "../components/ModalManager"; +import { + CommandPrefix_CPUNiceLevel, + CommandPrefix_CPUNiceLevelSchema, + CommandPrefix_IONiceLevel, + CommandPrefix_IONiceLevelSchema, + type Repo, + RepoSchema, + Schedule_Clock, +} from "../../gen/ts/v1/config_pb"; +import { StringValueSchema } from "../../gen/ts/types/value_pb"; +import { URIAutocomplete } from "../components/URIAutocomplete"; +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { formatErrorAlert, useAlertApi } from "../components/Alerts"; +import { namePattern, validateForm } from "../lib/formutil"; +import { backrestService } from "../api"; +import { + HooksFormList, + hooksListTooltipText, +} from "../components/HooksFormList"; +import { ConfirmButton, SpinButton } from "../components/SpinButton"; +import { useConfig } from "../components/ConfigProvider"; +import Cron from "react-js-cron"; +import { + ScheduleDefaultsInfrequent, + ScheduleFormItem, +} from "../components/ScheduleFormItem"; +import { isWindows } from "../state/buildcfg"; +import { create, fromJson, JsonValue, toJson } from "@bufbuild/protobuf"; + +const repoDefaults = create(RepoSchema, { + prunePolicy: { + maxUnusedPercent: 10, + schedule: { + schedule: { + case: "cron", + value: "0 0 1 * *", // 1st of the month, + }, + clock: Schedule_Clock.LAST_RUN_TIME, + }, + }, + checkPolicy: { + schedule: { + schedule: { + case: "cron", + value: "0 0 1 * *", // 1st of the month, + }, + clock: Schedule_Clock.LAST_RUN_TIME, + }, + }, + commandPrefix: { + ioNice: CommandPrefix_IONiceLevel.IO_DEFAULT, + cpuNice: CommandPrefix_CPUNiceLevel.CPU_DEFAULT, + }, +}); + +export const AddRepoModal = ({ template }: { template: Repo | null }) => { + const [confirmLoading, setConfirmLoading] = useState(false); + const showModal = useShowModal(); + const alertsApi = useAlertApi()!; + const [config, setConfig] = useConfig(); + const [form] = Form.useForm(); + useEffect(() => { + const initVal = template + ? toJson(RepoSchema, template, { + alwaysEmitImplicit: true, + }) + : toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }); + form.setFieldsValue(initVal); + }, [template]); + + if (!config) { + return null; + } + + const handleDestroy = async () => { + setConfirmLoading(true); + + try { + // Update config and notify success. + setConfig( + await backrestService.removeRepo( + create(StringValueSchema, { value: template!.id }) + ) + ); + showModal(null); + alertsApi.success( + "Deleted repo " + + template!.id! + + " from config but files remain. To release storage delete the files manually. URI: " + + template!.uri + ); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Operation error: "), 15); + } finally { + setConfirmLoading(false); + } + }; + + const handleOk = async () => { + setConfirmLoading(true); + + try { + let repoFormData = await validateForm(form); + const repo = fromJson(RepoSchema, repoFormData, { + ignoreUnknownFields: false, + }); + + if (template !== null) { + // We are in the update repo flow, update the repo via the service + setConfig(await backrestService.addRepo(repo)); + showModal(null); + alertsApi.success("Updated repo configuration " + repo.uri); + } else { + // We are in the create repo flow, create the new repo via the service + setConfig(await backrestService.addRepo(repo)); + showModal(null); + alertsApi.success("Added repo " + repo.uri); + } + + try { + // Update the snapshots for the repo to confirm the config works. + // TODO: this operation is only used here, find a different RPC for this purpose. + await backrestService.listSnapshots({ repoId: repo.id }); + } catch (e: any) { + alertsApi.error( + formatErrorAlert( + e, + "Failed to list snapshots for updated/added repo: " + ), + 10 + ); + } + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Operation error: "), 10); + } finally { + setConfirmLoading(false); + } + }; + + const handleCancel = () => { + showModal(null); + }; + + return ( + <> + + Cancel + , + template != null ? ( + + + Delete + + + ) : null, + { + let repoFormData = await validateForm(form); + console.log("checking repo", repoFormData); + const repo = fromJson(RepoSchema, repoFormData, { + ignoreUnknownFields: false, + }); + try { + const exists = await backrestService.checkRepoExists(repo); + if (exists.value) { + alertsApi.success( + "Connected successfully to " + + repo.uri + + " and found an existing repo.", + 10 + ); + } else { + alertsApi.success( + "Connected successfully to " + + repo.uri + + ". No existing repo found at this location, a new one will be initialized", + 10 + ); + } + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Check error: "), 10); + } + }} + > + Test Configuration + , + , + ]} + maskClosable={false} + > +

+ See{" "} + + backrest getting started guide + {" "} + for repository configuration instructions or check the{" "} + + restic documentation + {" "} + for more details about repositories. +

+
+
+ {/* Repo.id */} + + + hasFeedback + name="id" + label="Repo Name" + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input repo name", + }, + { + validator: async (_, value) => { + if (template) return; + if (config?.repos?.find((r) => r.id === value)) { + throw new Error(); + } + }, + message: "Repo with name already exists", + }, + { + pattern: namePattern, + message: + "Name must be alphanumeric with dashes or underscores as separators", + }, + ]} + > + + + + + name="guid" hidden> + + + + {/* Repo.uri */} + + + Valid Repo URIs are: +
    +
  • Local filesystem path
  • +
  • S3 e.g. s3:// ...
  • +
  • SFTP e.g. sftp:user@host:/repo-path
  • +
  • + See{" "} + + restic docs + {" "} + for more info. +
  • +
+ + } + > + + hasFeedback + name="uri" + label="Repository URI" + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input repo URI", + }, + ]} + > + + +
+ + {/* Repo.password */} + + This password that encrypts data in your repository. +
    +
  • + Recommended to pick a value that is 128 bits of entropy (20 + chars or longer) +
  • +
  • + You may alternatively provide env variable credentials e.g. + RESTIC_PASSWORD, RESTIC_PASSWORD_FILE, or + RESTIC_PASSWORD_COMMAND. +
  • +
  • + Click [Generate] to seed a random password from your + browser's crypto random API. +
  • +
+ + } + > + + + + + hasFeedback + name="password" + validateTrigger={["onChange", "onBlur"]} + > + + + + + + + + +
+ + {/* Repo.env */} + + + { + return await envVarSetValidator(form, envVars); + }, + }, + ]} + > + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => ( + + + form.validateFields()} + style={{ width: "90%" }} + /> + + remove(index)} + style={{ paddingLeft: "5px" }} + /> + + ))} + + + + + + )} + + + + + {/* Repo.flags */} + + + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => ( + + + + + remove(index)} + style={{ paddingLeft: "5px" }} + /> + + ))} + + + + + + )} + + + + {/* Repo.prunePolicy */} + + The schedule on which prune operations are run for this + repository. Read{" "} + + the restic docs on customizing prune operations + {" "} + for more details. + + } + > + Prune Policy + + } + > + + +
Max Unused After Prune
+ + } + /> +
+ +
+ + {/* Repo.checkPolicy */} + + The schedule on which check operations are run for this + repository. Restic check operations verify the integrity of + your repository by scanning the on-disk structures that make + up your backup data. Check can optionally be configured to + re-read and re-hash data, this is slow and can be bandwidth + expensive but will catch any bitrot or silent corruption in + the storage medium. + + } + > + Check Policy + + } + > + + +
Read Data %
+ + } + /> +
+ +
+ + {/* Repo.commandPrefix */} + {!isWindows && ( + + Modifiers for the backup operation e.g. set the CPU or IO + priority. + + } + > + Command Modifiers + + } + colon={false} + > + + + + Available IO priority modes +
    +
  • + IO_BEST_EFFORT_LOW - runs at lower than default disk + priority (will prioritize other processes) +
  • +
  • + IO_BEST_EFFORT_HIGH - runs at higher than default + disk priority (top of disk IO queue) +
  • +
  • + IO_IDLE - only runs when disk bandwidth is idle + (e.g. no other operations are queued) +
  • +
+ + } + > + IO Priority: +
+ + ({ + label: v.name, + value: v.name, + }) + )} + /> + +
+ +
+
+ )} + + + Auto Unlock + + } + name="autoUnlock" + valuePropName="checked" + > + + + + Hooks} + > + + + + + {() => ( + +
+                          {JSON.stringify(form.getFieldsValue(), undefined, 2)}
+                        
+ + ), + }, + ]} + /> + )} +
+ +
+ + ); +}; + +const expectedEnvVars: { [scheme: string]: string[][] } = { + s3: [ + ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], + ["AWS_SHARED_CREDENTIALS_FILE"], + ], + b2: [["B2_ACCOUNT_ID", "B2_ACCOUNT_KEY"]], + azure: [ + ["AZURE_ACCOUNT_NAME", "AZURE_ACCOUNT_KEY"], + ["AZURE_ACCOUNT_NAME", "AZURE_ACCOUNT_SAS"], + ], + gs: [ + ["GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_PROJECT_ID"], + ["GOOGLE_ACCESS_TOKEN"], + ], +}; + +const envVarSetValidator = (form: FormInstance, envVars: string[]) => { + if (!envVars) { + return Promise.resolve(); + } + + let uri = form.getFieldValue("uri"); + if (!uri) { + return Promise.resolve(); + } + + const envVarNames = envVars.map((e) => { + if (!e) { + return ""; + } + let idx = e.indexOf("="); + if (idx === -1) { + return ""; + } + return e.substring(0, idx); + }); + + // check that password is provided in some form + const password = form.getFieldValue("password"); + if ( + (!password || password.length === 0) && + !envVarNames.includes("RESTIC_PASSWORD") && + !envVarNames.includes("RESTIC_PASSWORD_COMMAND") && + !envVarNames.includes("RESTIC_PASSWORD_FILE") + ) { + return Promise.reject( + new Error( + "Missing repo password. Either provide a password or set one of the env variables RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE." + ) + ); + } + + // find expected env for scheme + let schemeIdx = uri.indexOf(":"); + if (schemeIdx === -1) { + return Promise.resolve(); + } + + let scheme = uri.substring(0, schemeIdx); + + return checkSchemeEnvVars(scheme, envVarNames); +}; + +const cryptoRandomPassword = (): string => { + let vals = crypto.getRandomValues(new Uint8Array(64)); + // 48 chars is at least log2(64) * 48 = ~288 bits of entropy. + return btoa(String.fromCharCode(...vals)).slice(0, 48); +}; + +const checkSchemeEnvVars = ( + scheme: string, + envVarNames: string[] +): Promise => { + let expected = expectedEnvVars[scheme]; + if (!expected) { + return Promise.resolve(); + } + + const missingVarsCollection: string[][] = []; + + for (let possibility of expected) { + const missingVars = possibility.filter( + (envVar) => !envVarNames.includes(envVar) + ); + + // If no env vars are missing, we have a full match and are good + if (missingVars.length === 0) { + return Promise.resolve(); + } + + // First pass: Only add those missing vars from sets where at least one existing env var already exists + if (missingVars.length < possibility.length) { + missingVarsCollection.push(missingVars); + } + } + + // If we didn't find any env var set with a partial match, then add all expected sets + if (!missingVarsCollection.length) { + missingVarsCollection.push(...expected); + } + + return Promise.reject( + new Error( + "Missing env vars " + + formatMissingEnvVars(missingVarsCollection) + + " for scheme " + + scheme + ) + ); +}; + +const formatMissingEnvVars = (partialMatches: string[][]): string => { + return partialMatches + .map((x) => { + if (x.length > 1) { + return `[ ${x.join(", ")} ]`; + } + return x[0]; + }) + .join(" or "); +}; + +const InputPercent = ({ ...props }) => { + return ( + + ); +}; diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx new file mode 100644 index 000000000..2315cded8 --- /dev/null +++ b/webui/src/views/App.tsx @@ -0,0 +1,458 @@ +import React, { Suspense, useEffect, useState } from "react"; +import { + ScheduleOutlined, + DatabaseOutlined, + PlusOutlined, + CheckCircleOutlined, + ExclamationOutlined, + SettingOutlined, + LoadingOutlined, +} from "@ant-design/icons"; +import type { MenuProps } from "antd"; +import { Button, Empty, Layout, Menu, Spin, theme } from "antd"; +import { Config } from "../../gen/ts/v1/config_pb"; +import { useAlertApi } from "../components/Alerts"; +import { useShowModal } from "../components/ModalManager"; +import { uiBuildVersion } from "../state/buildcfg"; +import { ActivityBar } from "../components/ActivityBar"; +import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations_pb"; +import { + subscribeToOperations, + unsubscribeFromOperations, +} from "../state/oplog"; +import LogoSvg from "url:../../assets/logo.svg"; +import _ from "lodash"; +import { Code } from "@connectrpc/connect"; +import { LoginModal } from "./LoginModal"; +import { backrestService, setAuthToken } from "../api"; +import { useConfig } from "../components/ConfigProvider"; +import { shouldShowSettings } from "../state/configutil"; +import { OpSelector, OpSelectorSchema } from "../../gen/ts/v1/service_pb"; +import { colorForStatus } from "../state/flowdisplayaggregator"; +import { getStatusForSelector, matchSelector } from "../state/logstate"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { MainContentAreaTemplate } from "./MainContentArea"; +import { create } from "@bufbuild/protobuf"; + +const { Header, Sider } = Layout; + +const SummaryDashboard = React.lazy(() => + import("./SummaryDashboard").then((m) => ({ + default: m.SummaryDashboard, + })) +); + +const GettingStartedGuide = React.lazy(() => + import("./GettingStartedGuide").then((m) => ({ + default: m.GettingStartedGuide, + })) +); + +const PlanView = React.lazy(() => + import("./PlanView").then((m) => ({ + default: m.PlanView, + })) +); + +const RepoView = React.lazy(() => + import("./RepoView").then((m) => ({ + default: m.RepoView, + })) +); + +const RepoViewContainer = () => { + const { repoId } = useParams(); + const [config, setConfig] = useConfig(); + + if (!config) { + return ; + } + + const repo = config.repos.find((r) => r.id === repoId); + + return ( + + {repo ? ( + + ) : ( + + )} + + ); +}; + +const PlanViewContainer = () => { + const { planId } = useParams(); + const [config, setConfig] = useConfig(); + + if (!config) { + return ; + } + + const plan = config.plans.find((p) => p.id === planId); + return ( + + {plan ? ( + + ) : ( + + )} + + ); +}; + +export const App: React.FC = () => { + const { + token: { colorBgContainer, colorTextLightSolid }, + } = theme.useToken(); + const navigate = useNavigate(); + const [config, setConfig] = useConfig(); + + const items = getSidenavItems(config); + + return ( + +
+ { + navigate("/"); + }} + > + + +

+ + + {uiBuildVersion} + + + + + +

+

+ + {config && config.instance ? config.instance : undefined} + + +

+
+ + + + + + }> + + + + + } + /> + + + + } + /> + } /> + } /> + + + + } + /> + + + + + + ); +}; + +const AuthenticationBoundary = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [config, setConfig] = useConfig(); + const alertApi = useAlertApi()!; + const showModal = useShowModal(); + + useEffect(() => { + backrestService + .getConfig({}) + .then((config) => { + setConfig(config); + if (shouldShowSettings(config)) { + import("./SettingsModal").then(({ SettingsModal }) => { + showModal(); + }); + } else { + showModal(null); + } + }) + .catch((err) => { + const code = err.code; + if (err.code === Code.Unauthenticated) { + showModal(); + return; + } else if ( + err.code !== Code.Unavailable && + err.code !== Code.DeadlineExceeded + ) { + alertApi.error(err.message, 0); + return; + } + + alertApi.error( + "Failed to fetch initial config, typically this means the UI could not connect to the backend", + 0 + ); + }); + }, []); + + if (!config) { + return <>; + } + + return <>{children}; +}; + +const getSidenavItems = (config: Config | null): MenuProps["items"] => { + const showModal = useShowModal(); + const navigate = useNavigate(); + + if (!config) { + return; + } + + const reposById = _.keyBy(config.repos, (r) => r.id); + const configPlans = config.plans || []; + const configRepos = config.repos || []; + + const plans: MenuProps["items"] = [ + { + key: "add-plan", + icon: , + label: "Add Plan", + onClick: async () => { + const { AddPlanModal } = await import("./AddPlanModal"); + showModal(); + }, + }, + ...configPlans.map((plan) => { + const sel = create(OpSelectorSchema, { + instanceId: config.instance, + planId: plan.id, + repoGuid: reposById[plan.repo]?.guid, + }); + + return { + key: "p-" + plan.id, + icon: , + label: ( +
+ {plan.id}{" "} +
+ ), + onClick: async () => { + navigate(`/plan/${plan.id}`); + }, + }; + }), + ]; + + const repos: MenuProps["items"] = [ + { + key: "add-repo", + icon: , + label: "Add Repo", + onClick: async () => { + const { AddRepoModal } = await import("./AddRepoModal"); + showModal(); + }, + }, + ...configRepos.map((repo) => { + return { + key: "r-" + repo.id, + icon: ( + + ), + label: ( +
+ {repo.id}{" "} +
+ ), + onClick: async () => { + navigate(`/repo/${repo.id}`); + }, + }; + }), + ]; + + return [ + { + key: "plans", + icon: React.createElement(ScheduleOutlined), + label: "Plans", + children: plans, + }, + { + key: "repos", + icon: React.createElement(DatabaseOutlined), + label: "Repositories", + children: repos, + }, + { + key: "settings", + icon: React.createElement(SettingOutlined), + label: "Settings", + onClick: async () => { + const { SettingsModal } = await import("./SettingsModal"); + showModal(); + }, + }, + ]; +}; + +const IconForResource = ({ selector }: { selector: OpSelector }) => { + const [status, setStatus] = useState(OperationStatus.STATUS_UNKNOWN); + useEffect(() => { + if (!selector || !selector.instanceId || !selector.repoGuid) { + return; + } + + const load = async () => { + setStatus(await getStatusForSelector(selector)); + }; + load(); + const refresh = _.debounce(load, 1000, { maxWait: 10000, trailing: true }); + const callback = (event?: OperationEvent, err?: Error) => { + if (!event || !event.event) return; + switch (event.event.case) { + case "createdOperations": + case "updatedOperations": + const ops = event.event.value.operations; + if (ops.find((op) => matchSelector(selector, op))) { + refresh(); + } + break; + case "deletedOperations": + refresh(); + break; + } + }; + + subscribeToOperations(callback); + return () => { + unsubscribeFromOperations(callback); + }; + }, [JSON.stringify(selector)]); + return iconForStatus(status); +}; + +const iconForStatus = (status: OperationStatus) => { + const color = colorForStatus(status); + switch (status) { + case OperationStatus.STATUS_ERROR: + return ; + case OperationStatus.STATUS_WARNING: + return ; + case OperationStatus.STATUS_INPROGRESS: + return ; + case OperationStatus.STATUS_UNKNOWN: + return ; + default: + return ; + } +}; diff --git a/webui/src/views/GettingStartedGuide.tsx b/webui/src/views/GettingStartedGuide.tsx new file mode 100644 index 000000000..8c8456576 --- /dev/null +++ b/webui/src/views/GettingStartedGuide.tsx @@ -0,0 +1,96 @@ +import { Collapse, Divider, Spin, Typography } from "antd"; +import React, { useEffect, useState } from "react"; +import { backrestService } from "../api"; +import { useConfig } from "../components/ConfigProvider"; +import { Config, ConfigSchema } from "../../gen/ts/v1/config_pb"; +import { isDevBuild } from "../state/buildcfg"; +import { toJsonString } from "@bufbuild/protobuf"; + +export const GettingStartedGuide = () => { + const config = useConfig()[0]; + + return ( + <> + +

Getting Started

+ {/* open link in new tab */} +

+ + Check for new Backrest releases on GitHub + +

+ Overview +
    +
  • + Repos map directly to restic repositories, start by configuring your + backup locations. +
  • +
  • + Plans are where you configure directories to backup, and backup + scheduling. Multiple plans can backup to a single restic repository. +
  • +
  • + See{" "} + + the restic docs on preparing a new repository + {" "} + for details about available repository types and how they can be + configured. +
  • +
  • + See{" "} + + the Backrest wiki + {" "} + for instructions on how to configure Backrest. +
  • +
+ Tips +
    +
  • + Backup your Backrest configuration: your Backrest config holds all + of your repos, plans, and the passwords to decrypt them. When you + have Backrest configured to your liking make sure to store a copy of + your config (or minimally a copy of your passwords) in a safe + location e.g. a secure note in your password manager. +
  • +
  • + Configure hooks: Backrest can deliver notifications about backup + events. It's strongly recommended that you configure an on error + hook that will notify you in the event that backups start failing + (e.g. an issue with storage or network connectivity). Hooks can be + configured either at the plan or repo level. +
  • +
+ {isDevBuild && ( + <> + Config View +
+ + ); +}; diff --git a/webui/src/views/LoginModal.tsx b/webui/src/views/LoginModal.tsx new file mode 100644 index 000000000..139c451ca --- /dev/null +++ b/webui/src/views/LoginModal.tsx @@ -0,0 +1,93 @@ +import { LockOutlined, UserOutlined } from "@ant-design/icons"; +import { Button, Col, Form, Input, Modal, Row } from "antd"; +import React, { useEffect, useState } from "react"; +import { authenticationService, setAuthToken } from "../api"; +import { + LoginRequest, + LoginRequestSchema, +} from "../../gen/ts/v1/authentication_pb"; +import { useAlertApi } from "../components/Alerts"; +import { create } from "@bufbuild/protobuf"; + +export const LoginModal = () => { + let defaultCreds = create(LoginRequestSchema, {}); + + const [form] = Form.useForm(); + const alertApi = useAlertApi()!; + + const onFinish = async (values: any) => { + const loginReq = create(LoginRequestSchema, { + username: values.username, + password: values.password, + }); + + try { + const loginResponse = await authenticationService.login(loginReq); + setAuthToken(loginResponse.token); + alertApi.success("Logged in", 5); + setTimeout(() => { + window.location.reload(); + }, 500); + } catch (e: any) { + alertApi.error("Login failed: " + (e.message ? e.message : "" + e), 10); + } + }; + + return ( + +
+ + + + } + placeholder="Username" + /> + + + + + + } + type="password" + placeholder="Password" + /> + + + + + + +
+
+ ); +}; diff --git a/webui/src/views/MainContentArea.tsx b/webui/src/views/MainContentArea.tsx new file mode 100644 index 000000000..87f9954bb --- /dev/null +++ b/webui/src/views/MainContentArea.tsx @@ -0,0 +1,39 @@ +import { Breadcrumb, Layout, Spin, theme } from "antd"; +import { Content } from "antd/es/layout/layout"; +import React from "react"; + +interface Breadcrumb { + title: string; + onClick?: () => void; +} + +export const MainContentAreaTemplate = ({ + breadcrumbs, + children, +}: { + breadcrumbs: Breadcrumb[]; + children: React.ReactNode; +}) => { + const { + token: { colorBgContainer }, + } = theme.useToken(); + + return ( + + + + {children} + + + ); +}; diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx new file mode 100644 index 000000000..384c903f1 --- /dev/null +++ b/webui/src/views/PlanView.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { Plan } from "../../gen/ts/v1/config_pb"; +import { Button, Flex, Tabs, Tooltip, Typography } from "antd"; +import { useAlertApi } from "../components/Alerts"; +import { MAX_OPERATION_HISTORY } from "../constants"; +import { backrestService } from "../api"; +import { + ClearHistoryRequestSchema, + DoRepoTaskRequest_Task, + DoRepoTaskRequestSchema, + GetOperationsRequestSchema, +} from "../../gen/ts/v1/service_pb"; +import { SpinButton } from "../components/SpinButton"; +import { useShowModal } from "../components/ModalManager"; +import { create } from "@bufbuild/protobuf"; +import { useConfig } from "../components/ConfigProvider"; +import { OperationListView } from "../components/OperationListView"; +import { OperationTreeView } from "../components/OperationTreeView"; + +export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { + const [config, _] = useConfig(); + const alertsApi = useAlertApi()!; + const showModal = useShowModal(); + const repo = config?.repos.find((r) => r.id === plan.repo); + + const handleBackupNow = async () => { + try { + await backrestService.backup({ value: plan.id }); + alertsApi.success("Backup scheduled."); + } catch (e: any) { + alertsApi.error("Failed to schedule backup: " + e.message); + } + }; + + const handleUnlockNow = async () => { + try { + alertsApi.info("Unlocking repo..."); + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: plan.repo!, + task: DoRepoTaskRequest_Task.UNLOCK, + }) + ); + alertsApi.success("Repo unlocked."); + } catch (e: any) { + alertsApi.error("Failed to unlock repo: " + e.message); + } + }; + + const handleClearErrorHistory = async () => { + try { + alertsApi.info("Clearing error history..."); + await backrestService.clearHistory( + create(ClearHistoryRequestSchema, { + selector: { + planId: plan.id, + repoGuid: repo!.guid, + }, + onlyFailed: true, + }) + ); + alertsApi.success("Error history cleared."); + } catch (e: any) { + alertsApi.error("Failed to clear error history: " + e.message); + } + }; + + if (!repo) { + return ( + <> + + Repo {plan.repo} for plan {plan.id} not found + + + ); + } + + return ( + <> + + {plan.id} + + + + Backup Now + + + + + + + Unlock Repo + + + + + Clear Error History + + + + + + + ), + destroyInactiveTabPane: true, + }, + { + key: "2", + label: "List View", + children: ( + <> +

Backup Action History

+ + + ), + destroyInactiveTabPane: true, + }, + ]} + /> + + ); +}; diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx new file mode 100644 index 000000000..d5bd58886 --- /dev/null +++ b/webui/src/views/RepoView.tsx @@ -0,0 +1,212 @@ +import React, { Suspense, useContext, useEffect, useState } from "react"; +import { Repo } from "../../gen/ts/v1/config_pb"; +import { Flex, Tabs, Tooltip, Typography, Button } from "antd"; +import { OperationListView } from "../components/OperationListView"; +import { OperationTreeView } from "../components/OperationTreeView"; +import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants"; +import { + DoRepoTaskRequest_Task, + DoRepoTaskRequestSchema, + GetOperationsRequestSchema, + OpSelectorSchema, +} from "../../gen/ts/v1/service_pb"; +import { backrestService } from "../api"; +import { SpinButton } from "../components/SpinButton"; +import { useConfig } from "../components/ConfigProvider"; +import { formatErrorAlert, useAlertApi } from "../components/Alerts"; +import { useShowModal } from "../components/ModalManager"; +import { create } from "@bufbuild/protobuf"; + +const StatsPanel = React.lazy(() => import("../components/StatsPanel")); + +export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { + const [config, _] = useConfig(); + const showModal = useShowModal(); + const alertsApi = useAlertApi()!; + + // Task handlers + const handleIndexNow = async () => { + try { + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: repo.id!, + task: DoRepoTaskRequest_Task.INDEX_SNAPSHOTS, + }) + ); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Failed to index snapshots: ")); + } + }; + + const handleUnlockNow = async () => { + try { + alertsApi.info("Unlocking repo..."); + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: repo.id!, + task: DoRepoTaskRequest_Task.UNLOCK, + }) + ); + alertsApi.success("Repo unlocked."); + } catch (e: any) { + alertsApi.error("Failed to unlock repo: " + e.message); + } + }; + + const handleStatsNow = async () => { + try { + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: repo.id!, + task: DoRepoTaskRequest_Task.STATS, + }) + ); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Failed to compute stats: ")); + } + }; + + const handlePruneNow = async () => { + try { + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: repo.id!, + task: DoRepoTaskRequest_Task.PRUNE, + }) + ); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Failed to prune: ")); + } + }; + + const handleCheckNow = async () => { + try { + await backrestService.doRepoTask( + create(DoRepoTaskRequestSchema, { + repoId: repo.id!, + task: DoRepoTaskRequest_Task.CHECK, + }) + ); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Failed to check: ")); + } + }; + + // Gracefully handle deletions by checking if the plan is still in the config. + let repoInConfig = config?.repos?.find((r) => r.id === repo.id); + if (!repoInConfig) { + return ( + <> + Repo was deleted +
{JSON.stringify(config, null, 2)}
+ + ); + } + repo = repoInConfig; + + const items = [ + { + key: "1", + label: "Tree View", + children: ( + <> + + + ), + destroyInactiveTabPane: true, + }, + { + key: "2", + label: "List View", + children: ( + <> +

Backup Action History

+ + + ), + destroyInactiveTabPane: true, + }, + { + key: "3", + label: "Stats", + children: ( + Loading...}> + + + ), + destroyInactiveTabPane: true, + }, + ]; + return ( + <> + + {repo.id} + + + + + + + + + Index Snapshots + + + + + + Unlock Repo + + + + + + Prune Now + + + + + + Check Now + + + + + + Compute Stats + + + + + + ); +}; diff --git a/webui/src/views/RunCommandModal.tsx b/webui/src/views/RunCommandModal.tsx new file mode 100644 index 000000000..ada8bbd5e --- /dev/null +++ b/webui/src/views/RunCommandModal.tsx @@ -0,0 +1,98 @@ +import { Button, Input, Modal, Space } from "antd"; +import React from "react"; +import { useShowModal } from "../components/ModalManager"; +import { backrestService } from "../api"; +import { SpinButton } from "../components/SpinButton"; +import { ConnectError } from "@connectrpc/connect"; +import { useAlertApi } from "../components/Alerts"; +import { + GetOperationsRequest, + GetOperationsRequestSchema, + RunCommandRequest, + RunCommandRequestSchema, +} from "../../gen/ts/v1/service_pb"; +import { Repo } from "../../gen/ts/v1/config_pb"; +import { OperationListView } from "../components/OperationListView"; +import { create } from "@bufbuild/protobuf"; +import { useConfig } from "../components/ConfigProvider"; + +interface Invocation { + command: string; + output: string; + error: string; +} + +export const RunCommandModal = ({ repo }: { repo: Repo }) => { + const [config, _] = useConfig(); + const showModal = useShowModal(); + const alertApi = useAlertApi()!; + const [command, setCommand] = React.useState(""); + const [running, setRunning] = React.useState(false); + + const handleCancel = () => { + showModal(null); + }; + + const doExecute = async () => { + if (!command) return; + setRunning(true); + + const toRun = command.trim(); + setCommand(""); + + try { + const opID = await backrestService.runCommand( + create(RunCommandRequestSchema, { + repoId: repo.id!, + command: toRun, + }) + ); + } catch (e: any) { + alertApi.error("Command failed: " + e.message); + } finally { + setRunning(false); + } + }; + + return ( + + + setCommand(e.target.value)} + onKeyUp={(e) => { + if (e.key === "Enter") { + doExecute(); + } + }} + /> + + Execute + + + {running && command ? ( + + Warning: another command is already running. Wait for it to finish + before running another operation that requires the repo lock. + + ) : null} + op.op.case === "operationRunCommand"} + /> + + ); +}; diff --git a/webui/src/views/SettingsModal.tsx b/webui/src/views/SettingsModal.tsx new file mode 100644 index 000000000..476bb6d8e --- /dev/null +++ b/webui/src/views/SettingsModal.tsx @@ -0,0 +1,271 @@ +import { + Form, + Modal, + Input, + Typography, + Select, + Button, + Tooltip, + Radio, + InputNumber, + Row, + Card, + Col, + Collapse, + Checkbox, +} from "antd"; +import React, { useEffect, useState } from "react"; +import { useShowModal } from "../components/ModalManager"; +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { formatErrorAlert, useAlertApi } from "../components/Alerts"; +import { namePattern, validateForm } from "../lib/formutil"; +import { useConfig } from "../components/ConfigProvider"; +import { authenticationService, backrestService } from "../api"; +import { clone, fromJson, toJson, toJsonString } from "@bufbuild/protobuf"; +import { + AuthSchema, + ConfigSchema, + UserSchema, +} from "../../gen/ts/v1/config_pb"; + +interface FormData { + auth: { + users: { + name: string; + passwordBcrypt: string; + needsBcrypt?: boolean; + }[]; + }; + instance: string; +} + +export const SettingsModal = () => { + let [config, setConfig] = useConfig(); + const showModal = useShowModal(); + const alertsApi = useAlertApi()!; + const [form] = Form.useForm(); + + if (!config) { + return null; + } + + const handleOk = async () => { + try { + // Validate form + let formData = await validateForm(form); + + if (formData.auth?.users) { + for (const user of formData.auth?.users) { + if (user.needsBcrypt) { + const hash = await authenticationService.hashPassword({ + value: user.passwordBcrypt, + }); + user.passwordBcrypt = hash.value; + delete user.needsBcrypt; + } + } + } + + // Update configuration + let newConfig = clone(ConfigSchema, config); + newConfig.auth = fromJson(AuthSchema, formData.auth, { + ignoreUnknownFields: false, + }); + newConfig.instance = formData.instance; + + if (!newConfig.auth?.users && !newConfig.auth?.disabled) { + throw new Error( + "At least one user must be configured or authentication must be disabled" + ); + } + + setConfig(await backrestService.setConfig(newConfig)); + alertsApi.success("Settings updated", 5); + setTimeout(() => { + window.location.reload(); + }, 500); + } catch (e: any) { + alertsApi.error(formatErrorAlert(e, "Operation error: "), 15); + console.error(e); + } + }; + + const handleCancel = () => { + showModal(null); + }; + + const users = config.auth?.users || []; + + return ( + <> + + Cancel + , + , + ]} + > +
+ {users.length > 0 || config.auth?.disabled ? null : ( + <> + Initial backrest setup! +

+ Backrest has detected that you do not have any users configured, + please add at least one user to secure the web interface. +

+

+ You can add more users later or, if you forget your password, + reset users by editing the configuration file (typically in + $HOME/.backrest/config.json) +

+ + )} + + + + + + + + + + + toJson(UserSchema, u, { alwaysEmitImplicit: true }) + ) || [] + } + > + {(fields, { add, remove }) => ( + <> + {fields.map((field, index) => { + return ( + + + + + + + + + { + form.setFieldValue( + ["auth", "users", index, "needsBcrypt"], + true + ); + form.setFieldValue( + ["auth", "users", index, "passwordBcrypt"], + "" + ); + }} + /> + + + + { + remove(field.name); + }} + /> + + + ); + })} + + + + + )} + + + + + {() => ( + +
+                          {JSON.stringify(form.getFieldsValue(), null, 2)}
+                        
+ + ), + }, + ]} + /> + )} +
+
+
+ + ); +}; diff --git a/webui/src/views/SummaryDashboard.tsx b/webui/src/views/SummaryDashboard.tsx new file mode 100644 index 000000000..a80969c56 --- /dev/null +++ b/webui/src/views/SummaryDashboard.tsx @@ -0,0 +1,295 @@ +import { + Button, + Card, + Col, + Collapse, + Descriptions, + Divider, + Empty, + Flex, + Row, + Spin, + Typography, +} from "antd"; +import React, { useEffect, useState } from "react"; +import { useConfig } from "../components/ConfigProvider"; +import { + SummaryDashboardResponse, + SummaryDashboardResponse_Summary, +} from "../../gen/ts/v1/service_pb"; +import { backrestService } from "../api"; +import { useAlertApi } from "../components/Alerts"; +import { + formatBytes, + formatDate, + formatDuration, + formatTime, +} from "../lib/formatting"; +import { + Bar, + BarChart, + Cell, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { colorForStatus } from "../state/flowdisplayaggregator"; +import { OperationStatus } from "../../gen/ts/v1/operations_pb"; +import { isMobile } from "../lib/browserutil"; +import { useNavigate } from "react-router"; +import { toJsonString } from "@bufbuild/protobuf"; +import { ConfigSchema } from "../../gen/ts/v1/config_pb"; + +export const SummaryDashboard = () => { + const config = useConfig()[0]; + const alertApi = useAlertApi()!; + const navigate = useNavigate(); + + const [summaryData, setSummaryData] = + useState(); + + useEffect(() => { + // Fetch summary data + const fetchData = async () => { + // check if the tab is in the foreground + if (document.hidden) { + return; + } + + try { + const data = await backrestService.getSummaryDashboard({}); + setSummaryData(data); + } catch (e) { + alertApi.error("Failed to fetch summary data: " + e); + } + }; + + fetchData(); + + const interval = setInterval(fetchData, 60000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (!config) { + return; + } + + if (config.repos.length === 0 && config.plans.length === 0) { + navigate("/getting-started"); + } + }, [config]); + + if (!summaryData) { + return ; + } + + return ( + <> + + Repos + {summaryData && summaryData.repoSummaries.length > 0 ? ( + summaryData.repoSummaries.map((summary) => ( + + )) + ) : ( + + )} + Plans + {summaryData && summaryData.planSummaries.length > 0 ? ( + summaryData.planSummaries.map((summary) => ( + + )) + ) : ( + + )} + + System Info + + + {config && + toJsonString(ConfigSchema, config, { prettySpaces: 2 })} + + ), + }, + ]} + /> + + + ); +}; + +const SummaryPanel = ({ + summary, +}: { + summary: SummaryDashboardResponse_Summary; +}) => { + const recentBackupsChart: { + idx: number; + time: number; + durationMs: number; + color: string; + bytesAdded: number; + }[] = []; + const recentBackups = summary.recentBackups!; + for (let i = 0; i < recentBackups.timestampMs.length; i++) { + const color = colorForStatus(recentBackups.status[i]); + recentBackupsChart.push({ + idx: i, + time: Number(recentBackups.timestampMs[i]), + durationMs: Number(recentBackups.durationMs[i]), + color: color, + bytesAdded: Number(recentBackups.bytesAdded[i]), + }); + } + while (recentBackupsChart.length < 60) { + recentBackupsChart.push({ + idx: recentBackupsChart.length, + time: 0, + durationMs: 0, + color: "white", + bytesAdded: 0, + }); + } + + const BackupChartTooltip = ({ active, payload, label }: any) => { + const idx = Number(label); + + const entry = recentBackupsChart[idx]; + if (!entry || entry.idx > recentBackups.timestampMs.length) { + return null; + } + + const isPending = + recentBackups.status[idx] === OperationStatus.STATUS_PENDING; + + return ( + + Backup at {formatTime(entry.time)}{" "} +
+ {isPending ? ( + + Scheduled, waiting. + + ) : ( + + Took {formatDuration(entry.durationMs)}, added{" "} + {formatBytes(entry.bytesAdded)} + + )} +
+ ); + }; + + const cardInfo: { key: number; label: string; children: React.ReactNode }[] = + []; + + cardInfo.push( + { + key: 1, + label: "Backups (30d)", + children: ( + <> + {summary.backupsSuccessLast30days ? ( + + {summary.backupsSuccessLast30days + ""} ok + + ) : undefined} + {summary.backupsFailed30days ? ( + + {summary.backupsFailed30days + ""} failed + + ) : undefined} + {summary.backupsWarningLast30days ? ( + + {summary.backupsWarningLast30days + ""} warning + + ) : undefined} + + ), + }, + { + key: 2, + label: "Bytes Scanned (30d)", + children: formatBytes(Number(summary.bytesScannedLast30days)), + }, + { + key: 3, + label: "Bytes Added (30d)", + children: formatBytes(Number(summary.bytesAddedLast30days)), + } + ); + + // check if mobile layout + if (!isMobile()) { + cardInfo.push( + { + key: 4, + label: "Next Scheduled Backup", + children: summary.nextBackupTimeMs + ? formatTime(Number(summary.nextBackupTimeMs)) + : "None Scheduled", + }, + { + key: 5, + label: "Bytes Scanned Avg", + children: formatBytes(Number(summary.bytesScannedAvg)), + }, + { + key: 6, + label: "Bytes Added Avg", + children: formatBytes(Number(summary.bytesAddedAvg)), + } + ); + } + + return ( + + + + + + + + + + {recentBackupsChart.map((entry, index) => ( + + ))} + + + + } cursor={false} /> + + + + + + ); +}; diff --git a/webui/tsconfig.json b/webui/tsconfig.json new file mode 100644 index 000000000..8c3f5763c --- /dev/null +++ b/webui/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*" + ], +} \ No newline at end of file diff --git a/webui/webui.go b/webui/webui.go new file mode 100644 index 000000000..3e92dcbd6 --- /dev/null +++ b/webui/webui.go @@ -0,0 +1,65 @@ +package webui + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "io" + "io/fs" + "net/http" + "strings" + "sync" + "time" +) + +var etagCacheMu sync.Mutex +var etagCache = make(map[string]string) + +func calcEtag(path string, data []byte) string { + etagCacheMu.Lock() + defer etagCacheMu.Unlock() + etag, ok := etagCache[path] + if ok { + return etag + } + + md5sum := md5.Sum(data) + etag = "\"" + hex.EncodeToString(md5sum[:]) + "\"" + etagCache[path] = etag + return etag +} + +func Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path += "index.html" + } + + f, err := content.Open(contentPrefix + r.URL.Path + ".gz") + if err == nil { + defer f.Close() + w.Header().Set("Content-Encoding", "gzip") + serveFile(f, w, r, r.URL.Path) + return + } + + f, err = content.Open(contentPrefix + r.URL.Path) + if err == nil { + defer f.Close() + serveFile(f, w, r, r.URL.Path) + return + } + + http.Error(w, "Not found", http.StatusNotFound) + }) +} + +func serveFile(f fs.File, w http.ResponseWriter, r *http.Request, path string) { + data, err := io.ReadAll(f) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("ETag", calcEtag(path, data)) + http.ServeContent(w, r, path, time.Time{}, bytes.NewReader(data)) +} diff --git a/webui/webui_test.go b/webui/webui_test.go new file mode 100644 index 000000000..c9229f23e --- /dev/null +++ b/webui/webui_test.go @@ -0,0 +1,43 @@ +package webui + +import ( + "net/http" + "net/http/httptest" + "runtime" + "testing" +) + +func TestEmbedNotEmpty(t *testing.T) { + files, err := content.ReadDir(contentPrefix) + if err != nil { + t.Fatalf("expected embedded files for WebUI, got error: %v", err) + } + + if len(files) == 0 { + t.Fatalf("expected >0 embedded files for WebUI, got %d", len(files)) + } +} + +func TestServeIndex(t *testing.T) { + handler := Handler() + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Windows doesn't have the gzip binary, so we skip compression during the + // go:generate step on Windows + if runtime.GOOS != "windows" && rr.Header().Get("Content-Encoding") != "gzip" { + t.Errorf("handler returned wrong content encoding: got %v want %v", + rr.Header().Get("Content-Encoding"), "gzip") + } +} diff --git a/webui/webuinix.go b/webui/webuinix.go new file mode 100644 index 000000000..204bd49c6 --- /dev/null +++ b/webui/webuinix.go @@ -0,0 +1,16 @@ +//go:build linux || darwin || freebsd +// +build linux darwin freebsd + +//go:generate npm run clean +//go:generate npm run build +//go:generate gzip -r dist + +package webui + +import ( + "embed" +) + +//go:embed dist +var content embed.FS +var contentPrefix = "dist" diff --git a/webui/webuiwin.go b/webui/webuiwin.go new file mode 100644 index 000000000..16aa311b1 --- /dev/null +++ b/webui/webuiwin.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +//go:generate npm run clean-windows +//go:generate npm run build-windows + +package webui + +import "embed" + +//go:embed dist-windows/* +var content embed.FS +var contentPrefix = "dist-windows"