diff --git a/.gitleaks.toml b/.gitleaks.toml index 271609429..406c8ab2b 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -41,10 +41,6 @@ paths = [ # named secret_value.toml — inputs to a manifest/invariant test. '''(^|/)shellstate/test/''', - # Documentation. The match is an illustrative connection string in a - # distributed-filesystem explainer. - '''(^|/)czech-file-knife/docs/''', - # A DETECTOR definition: the flagged line is the literal '-----BEGIN # PRIVATE KEY-----' inside a list of patterns this contract searches FOR. # The scanner matched the rule, not a key. diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json index 32c3a354a..1522027b3 100644 --- a/.hypatia-baseline.json +++ b/.hypatia-baseline.json @@ -55,14 +55,6 @@ "note": "Translation target fixture: Python source used to demonstrate julianiser Python-to-Julia translation patterns. This file exists to be translated AWAY from Python, not as functional Python code.", "tracking_issue": "hyperpolymath/developer-ecosystem#111" }, - { - "severity": "high", - "rule_module": "cicd_rules", - "type": "banned_language_file", - "file_pattern": "czech-file-knife/cfk-ios/**", - "note": "cfk-ios is the iOS File Provider Extension for czech-file-knife; requires Swift for NSFileProviderReplicatedExtension (no Tauri/Dioxus equivalent for this specific iOS API).", - "tracking_issue": "hyperpolymath/developer-ecosystem#111" - }, { "severity": "high", "rule_module": "cicd_rules", diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9481df2b0..0a148241e 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -16,6 +16,14 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. === [Unreleased] +==== Removed + +* `czech-file-knife/` extracted to its own repository, + https://github.com/hyperpolymath/czech-file-knife[hyperpolymath/czech-file-knife] + (it was only held here). The standalone, RSR-template-compliant tree is the + `czech-file-knife/` prefix at commit `60ba3be3`; history is preserved + with `git subtree split --prefix=czech-file-knife`. + ==== Fixed * fix(licence): developer-ecosystem — clear scaffold-placeholder leak diff --git a/czech-file-knife/.claude/CLAUDE.md b/czech-file-knife/.claude/CLAUDE.md deleted file mode 100644 index 9c8e5fd97..000000000 --- a/czech-file-knife/.claude/CLAUDE.md +++ /dev/null @@ -1,83 +0,0 @@ -## Machine-Readable Artefacts - -The following files in `.machine_readable/` contain structured project metadata: - -- `STATE.scm` - Current project state and progress -- `META.scm` - Architecture decisions and development practices -- `ECOSYSTEM.scm` - Position in the ecosystem and related projects -- `AGENTIC.scm` - AI agent interaction patterns -- `NEUROSYM.scm` - Neurosymbolic integration config -- `PLAYBOOK.scm` - Operational runbook - ---- - -# CLAUDE.md - AI Assistant Instructions - -## Language Policy (Hyperpolymath Standard) - -### ALLOWED Languages & Tools - -| Language/Tool | Use Case | Notes | -|---------------|----------|-------| -| **AffineScript** | Primary application code | Affine-typed, compiles to typed-wasm or ESM | -| **Bun** | JS runtime & package management (tier 1) | Default for all new work. Runs compiled ESM/JS directly — no bundler step. Uses an npm-compatible `package.json` plus `bun.lock` — both are expected, not anti-patterns. | -| **Rust** | Performance-critical, systems, WASM | Preferred for CLI tools | -| **Tauri 2.0+** | Mobile apps (iOS/Android) | Rust backend + web UI | -| **Dioxus** | Mobile apps (native UI) | Pure Rust, React-like | -| **Gleam** | Backend services | Runs on BEAM or compiles to JS | -| **Bash/POSIX Shell** | Scripts, automation | Keep minimal | -| **JavaScript** | Only where AffineScript cannot | MCP protocol glue, Bun APIs | -| **Nickel** | Configuration language | For complex configs | -| **Guile Scheme** | State/meta files | STATE.scm, META.scm, ECOSYSTEM.scm | -| **Julia** | Batch scripts, data processing | Per RSR | -| **OCaml** | AffineScript compiler | Language-specific | -| **Ada** | Safety-critical systems | Where required | - -### BANNED - Do Not Use - -| Banned | Replacement | -|--------|-------------| -| TypeScript | AffineScript | -| Deno | Bun | -| Node.js | Bun | -| npm | Bun | -| pnpm/yarn | Bun | -| Go | Rust | -| Python | Julia/Rust/AffineScript | -| Java/Kotlin | Rust/Tauri/Dioxus | -| Swift | Tauri/Dioxus | -| React Native | Tauri/Dioxus | -| Flutter/Dart | Tauri/Dioxus | - -### Mobile Development - -**No exceptions for Kotlin/Swift** - use Rust-first approach: - -1. **Tauri 2.0+** - Web UI (AffineScript) + Rust backend, MIT/Apache-2.0 -2. **Dioxus** - Pure Rust native UI, MIT/Apache-2.0 - -Both are FOSS with independent governance (no Big Tech). - -### Enforcement Rules - -1. **No new TypeScript files** - Convert existing TS to AffineScript -2. **Use `package.json` + `bun.lock` for JS runtime deps** - Bun is npm-compatible; a manifest is REQUIRED -3. **`bun install --production` for production deps** - resolved from `package.json`, pinned via `bun.lock` -4. **No Go code** - Use Rust instead -5. **No Python anywhere** - Use Julia for data/batch, Rust for systems, AffineScript for apps -6. **No Kotlin/Swift for mobile** - Use Tauri 2.0+ or Dioxus - -### Package Management - -- **Primary**: Guix (guix.scm) -- **Fallback**: Nix (flake.nix) -- **JS deps**: Bun (`package.json` + `bun.lock`). Declare tooling as a devDependency and run `bunx --no-install --bun ` — a bare `bunx ` can fetch an unpinned package and may start Node via its shebang. - -### Security Requirements - -- No MD5/SHA1 for security (use SHA256+) -- HTTPS only (no HTTP URLs) -- No hardcoded secrets -- SHA-pinned dependencies -- SPDX license headers on all files - diff --git a/czech-file-knife/.clusterfuzzlite/Dockerfile b/czech-file-knife/.clusterfuzzlite/Dockerfile deleted file mode 100644 index bc3c04ea0..000000000 --- a/czech-file-knife/.clusterfuzzlite/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# ClusterFuzzLite build environment for czech-file-knife -FROM gcr.io/oss-fuzz-base/base-builder-rust@sha256:73c1d5648db54100639339d411a5d192cbc8bf413ee91e843a07cf6f0e319dc7 - -COPY . $SRC/czech-file-knife -WORKDIR $SRC/czech-file-knife - -COPY .clusterfuzzlite/build.sh $SRC/ diff --git a/czech-file-knife/.clusterfuzzlite/build.sh b/czech-file-knife/.clusterfuzzlite/build.sh deleted file mode 100644 index f0c8f3e89..000000000 --- a/czech-file-knife/.clusterfuzzlite/build.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -eu -# SPDX-License-Identifier: MPL-2.0 -# Build script for ClusterFuzzLite - -cd $SRC/czech-file-knife - -# Build fuzz targets using cargo-fuzz -cargo +nightly fuzz build - -# Copy fuzz targets to $OUT -for target in $(cargo +nightly fuzz list); do - cp ./target/x86_64-unknown-linux-gnu/release/$target $OUT/ -done diff --git a/czech-file-knife/.clusterfuzzlite/project.yaml b/czech-file-knife/.clusterfuzzlite/project.yaml deleted file mode 100644 index 77e9a1901..000000000 --- a/czech-file-knife/.clusterfuzzlite/project.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# ClusterFuzzLite configuration for czech-file-knife -language: rust diff --git a/czech-file-knife/.editorconfig b/czech-file-knife/.editorconfig deleted file mode 100644 index f67573749..000000000 --- a/czech-file-knife/.editorconfig +++ /dev/null @@ -1,68 +0,0 @@ -# czech-file-knife - Editor Configuration -# https://editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false - -[*.adoc] -trim_trailing_whitespace = false - -[*.rs] -indent_size = 4 - -[*.ex] -indent_size = 2 - -[*.exs] -indent_size = 2 - -[*.zig] -indent_size = 4 - -[*.ada] -indent_size = 3 - -[*.adb] -indent_size = 3 - -[*.ads] -indent_size = 3 - -[*.hs] -indent_size = 2 - -[*.res] -indent_size = 2 - -[*.resi] -indent_size = 2 - -[*.ncl] -indent_size = 2 - -[*.rkt] -indent_size = 2 - -[*.scm] -indent_size = 2 - -[*.nix] -indent_size = 2 - -[Justfile] -indent_style = space -indent_size = 4 - -[justfile] -indent_style = space -indent_size = 4 diff --git a/czech-file-knife/.gitattributes b/czech-file-knife/.gitattributes deleted file mode 100644 index e860a85c1..000000000 --- a/czech-file-knife/.gitattributes +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# RSR-compliant .gitattributes - -* text=auto eol=lf - -# Source -*.rs text eol=lf diff=rust -*.ex text eol=lf diff=elixir -*.exs text eol=lf diff=elixir -*.jl text eol=lf -*.res text eol=lf -*.resi text eol=lf -*.ada text eol=lf diff=ada -*.adb text eol=lf diff=ada -*.ads text eol=lf diff=ada -*.hs text eol=lf -*.chpl text eol=lf -*.scm text eol=lf -*.ncl text eol=lf -*.nix text eol=lf - -# Docs -*.md text eol=lf diff=markdown -*.adoc text eol=lf -*.txt text eol=lf - -# Data -*.json text eol=lf -*.yaml text eol=lf -*.yml text eol=lf -*.toml text eol=lf - -# Config -.gitignore text eol=lf -.gitattributes text eol=lf -justfile text eol=lf -Makefile text eol=lf -Containerfile text eol=lf - -# Scripts -*.sh text eol=lf - -# Binary -*.png binary -*.jpg binary -*.gif binary -*.pdf binary -*.woff2 binary -*.zip binary -*.gz binary - -# Lock files -Cargo.lock text eol=lf -diff -flake.lock text eol=lf -diff diff --git a/czech-file-knife/.github/FUNDING.yml b/czech-file-knife/.github/FUNDING.yml deleted file mode 100644 index 688a442ca..000000000 --- a/czech-file-knife/.github/FUNDING.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Funding platforms for hyperpolymath projects -# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository - -github: hyperpolymath -ko_fi: hyperpolymath -liberapay: hyperpolymath diff --git a/czech-file-knife/.github/ISSUE_TEMPLATE/bug_report.md b/czech-file-knife/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 987aab6bc..000000000 --- a/czech-file-knife/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[Bug]: " -labels: 'bug, priority: unset, triage' -assignees: '' - ---- - -**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. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/czech-file-knife/.github/ISSUE_TEMPLATE/custom.md b/czech-file-knife/.github/ISSUE_TEMPLATE/custom.md deleted file mode 100644 index 48d5f81fa..000000000 --- a/czech-file-knife/.github/ISSUE_TEMPLATE/custom.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Custom issue template -about: Describe this issue template's purpose here. -title: '' -labels: '' -assignees: '' - ---- - - diff --git a/czech-file-knife/.github/ISSUE_TEMPLATE/documentation.md b/czech-file-knife/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 4fcb9f9fa..000000000 --- a/czech-file-knife/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Documentation -about: Report unclear, missing, or incorrect documentation -title: "[DOCS]: " -labels: 'documentation, priority: unset, triage' -assignees: '' - ---- - -name: Documentation -description: Report unclear, missing, or incorrect documentation -title: "[Docs]: " -labels: ["documentation", "triage"] -body: - - type: markdown - attributes: - value: | - Help us improve our documentation by reporting issues or gaps. - - - type: dropdown - id: type - attributes: - label: Documentation issue type - options: - - Missing (documentation doesn't exist) - - Incorrect (information is wrong) - - Unclear (confusing or hard to follow) - - Outdated (no longer accurate) - - Typo or grammar - validations: - required: true - - - type: input - id: location - attributes: - label: Location - description: Where is this documentation? (URL, file path, or section name) - placeholder: README.adoc, section "Installation" - validations: - required: true - - - type: textarea - id: description - attributes: - label: Description - description: What's the problem with the current documentation? - placeholder: Describe what's wrong or missing - validations: - required: true - - - type: textarea - id: suggestion - attributes: - label: Suggested improvement - description: How should it be fixed or improved? - placeholder: The documentation should say... - validations: - required: false - - - type: checkboxes - id: contribution - attributes: - label: Contribution - options: - - label: I would be willing to submit a PR to fix this - required: false diff --git a/czech-file-knife/.github/ISSUE_TEMPLATE/feature_request.md b/czech-file-knife/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 3e8fa7e7e..000000000 --- a/czech-file-knife/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: 'enhancement, priority: unset, triage' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/czech-file-knife/.github/ISSUE_TEMPLATE/question.md b/czech-file-knife/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index fd0e2a5cc..000000000 --- a/czech-file-knife/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: Question -about: Ask a question about usage or behaviour -title: "[QUESTION]: " -labels: question, triage -assignees: '' - ---- - -name: Question -description: Ask a question about usage or behaviour -title: "[Question]: " -labels: ["question", "triage"] -body: - - type: markdown - attributes: - value: | - Have a question? You can also ask in [Discussions](../discussions) for broader conversations. - - - type: textarea - id: question - attributes: - label: Your question - description: What would you like to know? - placeholder: How do I...? - validations: - required: true - - - type: textarea - id: context - attributes: - label: Context - description: Any relevant context that helps us answer your question - placeholder: I'm trying to achieve X and I've tried Y... - validations: - required: false - - - type: textarea - id: research - attributes: - label: What I've already tried - description: What have you already looked at or attempted? - placeholder: I've read the README and searched issues but... - validations: - required: false - - - type: checkboxes - id: checked - attributes: - label: Pre-submission checklist - options: - - label: I have searched existing issues and discussions - required: true - - label: I have read the documentation - required: true diff --git a/czech-file-knife/.github/dependabot.yml b/czech-file-knife/.github/dependabot.yml deleted file mode 100644 index 72c209756..000000000 --- a/czech-file-knife/.github/dependabot.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - groups: - actions: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "weekly" - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-patch"] - - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" diff --git a/czech-file-knife/.github/workflows/cflite_batch.yml b/czech-file-knife/.github/workflows/cflite_batch.yml deleted file mode 100644 index fa25c0e5d..000000000 --- a/czech-file-knife/.github/workflows/cflite_batch.yml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: ClusterFuzzLite batch fuzzing -on: - schedule: - - cron: '0 3 * * 0' # Weekly on Sunday at 3am UTC - workflow_dispatch: -permissions: read-all -jobs: - BatchFuzzing: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - sanitizer: [address] - steps: - - name: Build Fuzzers (${{ matrix.sanitizer }}) - id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 - with: - language: rust - sanitizer: ${{ matrix.sanitizer }} - - name: Run Fuzzers (${{ matrix.sanitizer }}) - id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - fuzz-seconds: 1800 - mode: batch - sanitizer: ${{ matrix.sanitizer }} diff --git a/czech-file-knife/.github/workflows/cflite_pr.yml b/czech-file-knife/.github/workflows/cflite_pr.yml deleted file mode 100644 index 2bed247ad..000000000 --- a/czech-file-knife/.github/workflows/cflite_pr.yml +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: ClusterFuzzLite PR fuzzing -on: - pull_request: - branches: [main] -permissions: read-all -jobs: - PR: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - sanitizer: [address] - steps: - - name: Build Fuzzers (${{ matrix.sanitizer }}) - id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 - with: - language: rust - sanitizer: ${{ matrix.sanitizer }} - - name: Run Fuzzers (${{ matrix.sanitizer }}) - id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - fuzz-seconds: 300 - mode: code-change - sanitizer: ${{ matrix.sanitizer }} diff --git a/czech-file-knife/.github/workflows/codeql.yml b/czech-file-knife/.github/workflows/codeql.yml deleted file mode 100644 index 5eb5987f1..000000000 --- a/czech-file-knife/.github/workflows/codeql.yml +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: CodeQL Security Analysis -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - schedule: - - cron: '0 6 1 * *' -permissions: read-all -jobs: - analyze: - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.28.1 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.28.1 - with: - category: "/language:${{ matrix.language }}" diff --git a/czech-file-knife/.github/workflows/container.yml b/czech-file-knife/.github/workflows/container.yml deleted file mode 100644 index 0b5709e98..000000000 --- a/czech-file-knife/.github/workflows/container.yml +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Container Build -on: - push: - tags: - - 'v*' - workflow_dispatch: -permissions: read-all -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - name: Log in to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - name: Build and push - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v5 - with: - context: . - file: ./Containerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/czech-file-knife/.github/workflows/ghcr-publish.yml b/czech-file-knife/.github/workflows/ghcr-publish.yml deleted file mode 100644 index 3cdd024ac..000000000 --- a/czech-file-knife/.github/workflows/ghcr-publish.yml +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Publish to GHCR -permissions: read-all -on: - release: - types: [published] - workflow_dispatch: -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write - attestations: write - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Install nerdctl and containerd - run: | - # Install containerd - sudo apt-get update - sudo apt-get install -y containerd - sudo systemctl start containerd - - # Install nerdctl full (includes containerd, CNI plugins, buildkit) - NERDCTL_VERSION=2.2.1 - curl -fsSL "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-full-${NERDCTL_VERSION}-linux-amd64.tar.gz" -o /tmp/nerdctl.tar.gz - sudo tar -xzf /tmp/nerdctl.tar.gz -C /usr/local - sudo mkdir -p /opt/cni/bin - sudo cp /usr/local/libexec/cni/* /opt/cni/bin/ 2>/dev/null || true - - # Start buildkitd daemon - sudo /usr/local/bin/buildkitd & - sleep 3 - - name: Log in to GHCR - run: | - echo "${{ secrets.GITHUB_TOKEN }}" | sudo nerdctl login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Build image - run: | - sudo nerdctl build -f Containerfile -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} . - sudo nerdctl tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - name: Push image - run: | - sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - name: Tag release version - if: github.event_name == 'release' - run: | - VERSION=${{ github.event.release.tag_name }} - sudo nerdctl tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION} - sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION} diff --git a/czech-file-knife/.github/workflows/governance.yml b/czech-file-knife/.github/workflows/governance.yml deleted file mode 100644 index 2d0fcb2df..000000000 --- a/czech-file-knife/.github/workflows/governance.yml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# governance.yml — single wrapper calling the shared estate governance bundle -# in hyperpolymath/standards instead of carrying per-repo copies. -# -# Replaces the per-repo governance scaffolding removed in the same commit: -# quality.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml, -# security-policy.yml, rsr-antipattern.yml, wellknown-enforcement.yml, -# workflow-linter.yml -# -# Load-bearing build/security workflows stay standalone in the repo -# (rust-ci, codeql, dependabot, release, scan/mirror/pages plumbing). - -name: Governance -on: - push: - branches: [main, master] - pull_request: - workflow_dispatch: -permissions: - contents: read -jobs: - governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@210f14e753c80064ec1bcae72f1d654dd9b0e687 diff --git a/czech-file-knife/.github/workflows/hypatia-scan.yml b/czech-file-knife/.github/workflows/hypatia-scan.yml deleted file mode 100644 index 5db7732bc..000000000 --- a/czech-file-knife/.github/workflows/hypatia-scan.yml +++ /dev/null @@ -1,125 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Hypatia Neurosymbolic CI/CD Security Scan -name: Hypatia Security Scan -on: - push: - branches: [main, master, develop] - pull_request: - branches: [main, master] - schedule: - - cron: '0 0 * * 0' # Weekly on Sunday - workflow_dispatch: -permissions: read-all -jobs: - scan: - name: Hypatia Neurosymbolic Analysis - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - with: - fetch-depth: 0 # Full history for better pattern analysis - - name: Setup Elixir for Hypatia scanner - uses: erlef/setup-beam@2f0cc07b4b9bea248ae098aba9e1a8a1de5ec24c # v1.18.2 - with: - elixir-version: '1.19.4' - otp-version: '28.3' - - name: Clone Hypatia - run: | - if [ ! -d "$HOME/hypatia" ]; then - git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" - fi - - name: Build Hypatia scanner (if needed) - working-directory: ${{ env.HOME }}/hypatia - run: | - if [ ! -f hypatia-v2 ]; then - echo "Building hypatia-v2 scanner..." - cd scanner - mix deps.get - mix escript.build - mv hypatia ../hypatia-v2 - fi - - name: Run Hypatia scan - id: scan - run: | - echo "Scanning repository: ${{ github.repository }}" - - # Run scanner - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json - - # Count findings - FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) - echo "findings_count=$FINDING_COUNT" >> $GITHUB_OUTPUT - - # Extract severity counts - CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json) - HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json) - MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json) - - echo "critical=$CRITICAL" >> $GITHUB_OUTPUT - echo "high=$HIGH" >> $GITHUB_OUTPUT - echo "medium=$MEDIUM" >> $GITHUB_OUTPUT - - echo "## Hypatia Scan Results" >> $GITHUB_STEP_SUMMARY - echo "- Total findings: $FINDING_COUNT" >> $GITHUB_STEP_SUMMARY - echo "- Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY - echo "- High: $HIGH" >> $GITHUB_STEP_SUMMARY - echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY - - name: Upload findings artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: hypatia-findings - path: hypatia-findings.json - retention-days: 90 - - name: Submit findings to gitbot-fleet (Phase 2) - if: steps.scan.outputs.findings_count > 0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SHA: ${{ github.sha }} - run: "echo \"\U0001F4E4 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet...\"\n\n# Clone gitbot-fleet to temp directory\nFLEET_DIR=\"/tmp/gitbot-fleet-$$\"\ngit clone https://github.com/hyperpolymath/gitbot-fleet.git \"$FLEET_DIR\"\n\n# Run submission script\nbash \"$FLEET_DIR/scripts/submit-finding.sh\" hypatia-findings.json\n\n# Cleanup\nrm -rf \"$FLEET_DIR\"\n\necho \"✅ Finding submission complete\"\n" - - name: Check for critical issues - if: steps.scan.outputs.critical > 0 - run: | - echo "⚠️ Critical security issues found!" - echo "Review hypatia-findings.json for details" - # Don't fail the build yet - just warn - # exit 1 - - name: Generate scan report - run: | - cat << EOF > hypatia-report.md - # Hypatia Security Scan Report - - **Repository:** ${{ github.repository }} - **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") - **Commit:** ${{ github.sha }} - - ## Summary - - | Severity | Count | - |----------|-------| - | Critical | ${{ steps.scan.outputs.critical }} | - | High | ${{ steps.scan.outputs.high }} | - | Medium | ${{ steps.scan.outputs.medium }} | - | **Total**| ${{ steps.scan.outputs.findings_count }} | - - ## Next Steps - - 1. Review findings in the artifact: hypatia-findings.json - 2. Auto-fixable issues will be addressed by robot-repo-automaton (Phase 3) - 3. Manual review required for complex issues - - ## Learning - - These findings feed Hypatia's learning engine to improve future rules. - - --- - *Powered by [Hypatia](https://github.com/hyperpolymath/hypatia) - Neurosymbolic CI/CD Intelligence* - EOF - - cat hypatia-report.md >> $GITHUB_STEP_SUMMARY - - name: Comment on PR with findings - if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 - with: - script: "const fs = require('fs');\nconst findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));\n\nconst critical = findings.filter(f => f.severity === 'critical').length;\nconst high = findings.filter(f => f.severity === 'high').length;\n\nlet comment = `## \U0001F50D Hypatia Security Scan\\n\\n`;\ncomment += `**Findings:** ${findings.length} issues detected\\n\\n`;\ncomment += `| Severity | Count |\\n|----------|-------|\\n`;\ncomment += `| \U0001F534 Critical | ${critical} |\\n`;\ncomment += `| \U0001F7E0 High | ${high} |\\n`;\ncomment += `| \U0001F7E1 Medium | ${findings.length - critical - high} |\\n\\n`;\n\nif (critical > 0) {\n comment += `⚠️ **Action Required:** Critical security issues found!\\n\\n`;\n}\n\ncomment += `
View findings\\n\\n`;\ncomment += `\\`\\`\\`json\\n${JSON.stringify(findings.slice(0, 10), null, 2)}\\n\\`\\`\\`\\n`;\ncomment += `
\\n\\n`;\ncomment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`;\n\ngithub.rest.issues.createComment({\n owner: context.repo.owner,\n repo: context.repo.repo,\n issue_number: context.issue.number,\n body: comment\n});\n" diff --git a/czech-file-knife/.github/workflows/instant-sync.yml b/czech-file-knife/.github/workflows/instant-sync.yml deleted file mode 100644 index f9db7f45c..000000000 --- a/czech-file-knife/.github/workflows/instant-sync.yml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Instant Forge Sync - Triggers propagation to all forges on push/release -name: Instant Sync -on: - push: - branches: [main, master] - release: - types: [published] -permissions: - contents: read -jobs: - dispatch: - runs-on: ubuntu-latest - steps: - - name: Trigger Propagation - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v3 - with: - token: ${{ secrets.FARM_DISPATCH_TOKEN }} - repository: hyperpolymath/.git-private-farm - event-type: propagate - client-payload: |- - { - "repo": "${{ github.event.repository.name }}", - "ref": "${{ github.ref }}", - "sha": "${{ github.sha }}", - "forges": "" - } - - name: Confirm - run: echo "::notice::Propagation triggered for ${{ github.event.repository.name }}" diff --git a/czech-file-knife/.github/workflows/mirror.yml b/czech-file-knife/.github/workflows/mirror.yml deleted file mode 100644 index 9e41bcbbe..000000000 --- a/czech-file-knife/.github/workflows/mirror.yml +++ /dev/null @@ -1,120 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -name: Mirror to Git Forges -on: - push: - branches: [main] - workflow_dispatch: -permissions: read-all -jobs: - mirror-gitlab: - runs-on: ubuntu-latest - if: vars.GITLAB_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.GITLAB_SSH_KEY }} - - name: Mirror to GitLab - run: | - ssh-keyscan -t ed25519 gitlab.com >> ~/.ssh/known_hosts - git remote add gitlab git@gitlab.com:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force gitlab main - mirror-bitbucket: - runs-on: ubuntu-latest - if: vars.BITBUCKET_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.BITBUCKET_SSH_KEY }} - - name: Mirror to Bitbucket - run: | - ssh-keyscan -t ed25519 bitbucket.org >> ~/.ssh/known_hosts - git remote add bitbucket git@bitbucket.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force bitbucket main - mirror-codeberg: - runs-on: ubuntu-latest - if: vars.CODEBERG_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.CODEBERG_SSH_KEY }} - - name: Mirror to Codeberg - run: | - ssh-keyscan -t ed25519 codeberg.org >> ~/.ssh/known_hosts - git remote add codeberg git@codeberg.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force codeberg main - mirror-sourcehut: - runs-on: ubuntu-latest - if: vars.SOURCEHUT_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.SOURCEHUT_SSH_KEY }} - - name: Mirror to SourceHut - run: | - ssh-keyscan -t ed25519 git.sr.ht >> ~/.ssh/known_hosts - git remote add sourcehut git@git.sr.ht:~hyperpolymath/${{ github.event.repository.name }} || true - git push --force sourcehut main - mirror-disroot: - runs-on: ubuntu-latest - if: vars.DISROOT_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.DISROOT_SSH_KEY }} - - name: Mirror to Disroot - run: | - ssh-keyscan -t ed25519 git.disroot.org >> ~/.ssh/known_hosts - git remote add disroot git@git.disroot.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force disroot main - mirror-gitea: - runs-on: ubuntu-latest - if: vars.GITEA_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.GITEA_SSH_KEY }} - - name: Mirror to Gitea - run: | - ssh-keyscan -t ed25519 ${{ vars.GITEA_HOST }} >> ~/.ssh/known_hosts - git remote add gitea git@${{ vars.GITEA_HOST }}:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force gitea main - mirror-radicle: - runs-on: ubuntu-latest - if: vars.RADICLE_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - name: Setup Rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable - with: - toolchain: stable - - name: Install Radicle - run: | - # Install via cargo (safer than curl|sh) - cargo install radicle-cli --locked - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: Mirror to Radicle - run: | - echo "${{ secrets.RADICLE_KEY }}" > ~/.radicle/keys/radicle - chmod 600 ~/.radicle/keys/radicle - rad sync --announce || echo "Radicle sync attempted" diff --git a/czech-file-knife/.github/workflows/publish.yml b/czech-file-knife/.github/workflows/publish.yml deleted file mode 100644 index fcea8d134..000000000 --- a/czech-file-knife/.github/workflows/publish.yml +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 hyperpolymath -# -# Publish workspace crates to crates.io on release tags -# Publishes in dependency order: core → providers → cache → search → vfs → integrations → cli -# Requires: CARGO_REGISTRY_TOKEN secret - -name: Publish to crates.io -on: - push: - tags: - - 'v[0-9]+.*' - workflow_dispatch: - inputs: - dry_run: - description: 'Dry run (no actual publish)' - required: false - default: 'true' - type: boolean - crate: - description: 'Specific crate to publish (leave empty for all)' - required: false - default: '' - type: string -permissions: read-all -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 -jobs: - publish: - name: Publish crates - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable - with: - toolchain: stable - - name: Cache cargo - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y pkg-config libssl-dev libfuse3-dev libsqlite3-dev - - name: Verify workspace builds - run: cargo build --workspace --release - - name: Run workspace tests - run: cargo test --workspace - # Publish in dependency order with delays for index updates - - name: Publish cfk-core - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-core' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-core --dry-run - else - cargo publish -p cfk-core - sleep 30 # Wait for crates.io index update - fi - - name: Publish cfk-providers - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-providers' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-providers --dry-run - else - cargo publish -p cfk-providers - sleep 30 - fi - - name: Publish cfk-cache - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-cache' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-cache --dry-run - else - cargo publish -p cfk-cache - sleep 30 - fi - - name: Publish cfk-search - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-search' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-search --dry-run - else - cargo publish -p cfk-search - sleep 30 - fi - - name: Publish cfk-vfs - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-vfs' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-vfs --dry-run - else - cargo publish -p cfk-vfs - sleep 30 - fi - - name: Publish cfk-integrations - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-integrations' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-integrations --dry-run - else - cargo publish -p cfk-integrations - sleep 30 - fi - - name: Publish cfk-cli - if: github.event.inputs.crate == '' || github.event.inputs.crate == 'cfk-cli' - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then - cargo publish -p cfk-cli --dry-run - else - cargo publish -p cfk-cli - fi - - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 - with: - generate_release_notes: true - draft: false diff --git a/czech-file-knife/.github/workflows/release.yml b/czech-file-knife/.github/workflows/release.yml deleted file mode 100644 index 35488bfbf..000000000 --- a/czech-file-knife/.github/workflows/release.yml +++ /dev/null @@ -1,151 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 hyperpolymath - -name: Release -on: - push: - tags: ['v*'] - workflow_dispatch: - inputs: - tag: - description: 'Release tag (e.g., v0.1.0)' - required: true -permissions: read-all -env: - CARGO_TERM_COLOR: always -jobs: - build-binaries: - name: Build ${{ matrix.target }} - runs-on: ${{ matrix.os }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - binary: cfk - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - binary: cfk - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - binary: cfk - - target: x86_64-apple-darwin - os: macos-latest - binary: cfk - - target: aarch64-apple-darwin - os: macos-latest - binary: cfk - - target: x86_64-pc-windows-msvc - os: windows-latest - binary: cfk.exe - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable - with: - targets: ${{ matrix.target }} - - name: Install cross-compilation tools - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu - - name: Configure aarch64 linker - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - - name: Install musl tools - if: matrix.target == 'x86_64-unknown-linux-musl' - run: | - sudo apt-get update - sudo apt-get install -y musl-tools - - name: Install FUSE (Linux) - if: runner.os == 'Linux' - run: sudo apt-get install -y libfuse3-dev - - name: Install macFUSE (macOS) - if: runner.os == 'macOS' - run: brew install macfuse || true - - name: Build - run: cargo build --release --package cfk-cli --target ${{ matrix.target }} - - name: Package (Unix) - if: runner.os != 'Windows' - run: | - cd target/${{ matrix.target }}/release - tar -czvf cfk-${{ github.ref_name }}-${{ matrix.target }}.tar.gz ${{ matrix.binary }} - sha256sum cfk-${{ github.ref_name }}-${{ matrix.target }}.tar.gz > cfk-${{ github.ref_name }}-${{ matrix.target }}.tar.gz.sha256 - - name: Package (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - cd target/${{ matrix.target }}/release - Compress-Archive -Path ${{ matrix.binary }} -DestinationPath cfk-${{ github.ref_name }}-${{ matrix.target }}.zip - Get-FileHash cfk-${{ github.ref_name }}-${{ matrix.target }}.zip -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) cfk-${{ github.ref_name }}-${{ matrix.target }}.zip" } | Out-File -FilePath cfk-${{ github.ref_name }}-${{ matrix.target }}.zip.sha256 - - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4 - with: - name: cfk-${{ matrix.target }} - path: | - target/${{ matrix.target }}/release/cfk-${{ github.ref_name }}-${{ matrix.target }}.* - build-deb: - name: Build .deb package - runs-on: ubuntu-latest - permissions: - contents: read - needs: build-binaries - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Install cargo-deb - run: cargo install cargo-deb - - name: Build .deb - run: cargo deb --package cfk-cli - - name: Upload .deb - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4 - with: - name: deb-package - path: target/debian/*.deb - build-rpm: - name: Build .rpm package - runs-on: ubuntu-latest - permissions: - contents: read - needs: build-binaries - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Install cargo-generate-rpm - run: cargo install cargo-generate-rpm - - name: Build binary - run: cargo build --release --package cfk-cli - - name: Strip binary - run: strip target/release/cfk - - name: Build .rpm - run: cargo generate-rpm --package cfk-cli - - name: Upload .rpm - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4 - with: - name: rpm-package - path: target/generate-rpm/*.rpm - release: - name: Create Release - runs-on: ubuntu-latest - needs: [build-binaries, build-deb, build-rpm] - permissions: - contents: write - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Download all artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v4 - with: - path: artifacts - - name: Collect release assets - run: | - mkdir -p release - find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" -o -name "*.deb" -o -name "*.rpm" \) -exec cp {} release/ \; - ls -la release/ - - name: Create Release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 - with: - files: release/* - generate_release_notes: true - draft: false diff --git a/czech-file-knife/.github/workflows/scorecard-enforcer.yml b/czech-file-knife/.github/workflows/scorecard-enforcer.yml deleted file mode 100644 index 9632349ff..000000000 --- a/czech-file-knife/.github/workflows/scorecard-enforcer.yml +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Prevention workflow - runs OpenSSF Scorecard and fails on low scores -name: OpenSSF Scorecard Enforcer -on: - push: - branches: [main] - schedule: - - cron: '0 6 * * 1' # Weekly on Monday - workflow_dispatch: -permissions: read-all -jobs: - scorecard: - runs-on: ubuntu-latest - permissions: - security-events: write - id-token: write # For OIDC - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - persist-credentials: false - - name: Run Scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 - with: - sarif_file: results.sarif - # Check specific high-priority items - check-critical: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Check SECURITY.md exists - run: | - if [ ! -f "SECURITY.md" ]; then - echo "::error::SECURITY.md is required" - exit 1 - fi - - name: Check for pinned dependencies - run: | - # Check workflows for unpinned actions - unpinned=$(grep -r "uses:.*@v[0-9]" .github/workflows/*.yml 2>/dev/null | grep -v "#" | head -5 || true) - if [ -n "$unpinned" ]; then - echo "::warning::Found unpinned actions:" - echo "$unpinned" - fi diff --git a/czech-file-knife/.github/workflows/scorecard.yml b/czech-file-knife/.github/workflows/scorecard.yml deleted file mode 100644 index 86b2ab8ab..000000000 --- a/czech-file-knife/.github/workflows/scorecard.yml +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: OSSF Scorecard -on: - push: - branches: [main, master] - schedule: - - cron: '0 4 * * *' - workflow_dispatch: -permissions: read-all -jobs: - analysis: - runs-on: ubuntu-latest - permissions: - security-events: write - id-token: write - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - name: Run Scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.3.1 - with: - results_file: results.sarif - results_format: sarif - - name: Upload results - uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.31.8 - with: - sarif_file: results.sarif diff --git a/czech-file-knife/.github/workflows/secret-scanner.yml b/czech-file-knife/.github/workflows/secret-scanner.yml deleted file mode 100644 index efdb0a29a..000000000 --- a/czech-file-knife/.github/workflows/secret-scanner.yml +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Prevention workflow - scans for hardcoded secrets before they reach main -name: Secret Scanner -on: - pull_request: - push: - branches: [main] -permissions: read-all -jobs: - trufflehog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 # Full history for scanning - - name: TruffleHog Secret Scan - uses: trufflesecurity/trufflehog@8a8ef8526528d8a4ff3e2c90be08e25ef8efbd9b # v3 - with: - extra_args: --only-verified --fail - # Rust-specific: Check for hardcoded crypto values - rust-secrets: - runs-on: ubuntu-latest - if: ${{ hashFiles('**/Cargo.toml') != '' }} - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Check for hardcoded secrets in Rust - run: | - # Patterns that suggest hardcoded secrets - PATTERNS=( - 'const.*SECRET.*=.*"' - 'const.*KEY.*=.*"[a-zA-Z0-9]{16,}"' - 'const.*TOKEN.*=.*"' - 'let.*api_key.*=.*"' - 'HMAC.*"[a-fA-F0-9]{32,}"' - 'password.*=.*"[^"]+"' - ) - - found=0 - for pattern in "${PATTERNS[@]}"; do - if grep -rn --include="*.rs" -E "$pattern" src/; then - echo "WARNING: Potential hardcoded secret found matching: $pattern" - found=1 - fi - done - - if [ $found -eq 1 ]; then - echo "::error::Potential hardcoded secrets detected. Use environment variables instead." - exit 1 - fi diff --git a/czech-file-knife/.github/workflows/stress-test.yml b/czech-file-knife/.github/workflows/stress-test.yml deleted file mode 100644 index 028860ff8..000000000 --- a/czech-file-knife/.github/workflows/stress-test.yml +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Stress Testing -on: - schedule: - - cron: '0 3 * * 1' # Weekly Monday 3am UTC - workflow_dispatch: -permissions: read-all -jobs: - stress-test: - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - with: - toolchain: stable - - name: Install stress testing tools - run: | - sudo apt-get update - sudo apt-get install -y stress-ng valgrind - - name: Build release - run: cargo build --release --all-features - - name: Concurrent operations stress test - run: | - # Run binary with high concurrency - for i in {1..100}; do - timeout 1s ./target/release/* & - done - wait - - name: Memory pressure test - run: | - # Run under memory constraints - ulimit -v 512000 # 500MB virtual memory limit - cargo test --release - - name: Long-running scenario test - run: | - # Test for memory leaks over time - timeout 300s valgrind --leak-check=full --error-exitcode=1 \ - ./target/release/* || true - - name: Stress test with stress-ng - run: | - # CPU and I/O stress - stress-ng --cpu 4 --io 2 --timeout 60s & - STRESS_PID=$! - cargo test --release - kill $STRESS_PID || true diff --git a/czech-file-knife/.gitignore b/czech-file-knife/.gitignore deleted file mode 100644 index 73f3573f8..000000000 --- a/czech-file-knife/.gitignore +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# RSR-compliant .gitignore - -# OS & Editor -.DS_Store -Thumbs.db -*.swp -*.swo -*~ -.idea/ -.vscode/ - -# Build -/target/ -/_build/ -/build/ -/dist/ -/out/ - -# Dependencies -/node_modules/ -/vendor/ -/deps/ -/.elixir_ls/ - -# Rust -# Cargo.lock # Keep for binaries - -# Elixir -/cover/ -/doc/ -*.ez -erl_crash.dump - -# Julia -*.jl.cov -*.jl.mem -/Manifest.toml - -# ReScript -/lib/bs/ -/.bsb.lock - -# Python (SaltStack only) -__pycache__/ -*.py[cod] -.venv/ - -# Ada/SPARK -*.ali -/obj/ -/bin/ - -# Haskell -/.stack-work/ -/dist-newstyle/ - -# Chapel -*.chpl.tmp.* - -# Secrets -.env -.env.* -*.pem -*.key -secrets/ - -# Test/Coverage -/coverage/ -htmlcov/ - -# Logs -*.log -/logs/ - -# Temp -/tmp/ -*.tmp -*.bak - -# Crash recovery artifacts -ai-cli-crash-capture/ -target/ -node_modules/ -_build/ -deps/ -.elixir_ls/ -.cache/ -build/ -dist/ diff --git a/czech-file-knife/.gitlab-ci.yml b/czech-file-knife/.gitlab-ci.yml deleted file mode 100644 index 7f129180b..000000000 --- a/czech-file-knife/.gitlab-ci.yml +++ /dev/null @@ -1,163 +0,0 @@ -# GitLab CI/CD Configuration - -stages: - - lint - - test - - build - - security - - deploy -variables: - DOCKER_DRIVER: overlay2 - DOCKER_TLS_CERTDIR: "/certs" - PODMAN_USERNS: keep-id -# Default settings -default: - image: alpine:latest - before_script: - - echo "Starting job..." - after_script: - - echo "Job completed." -# Templates -.cache_template: &cache_config - cache: - key: ${CI_COMMIT_REF_SLUG} - paths: - - node_modules/ - - .cache/ - - vendor/ -# Lint stage -lint:shell: - stage: lint - image: koalaman/shellcheck-alpine:stable - script: - - shellcheck **/*.sh - only: - changes: - - "**/*.sh" -lint:yaml: - stage: lint - image: sdesbure/yamllint - script: - - yamllint -c .yamllint . - only: - changes: - - "**/*.yml" - - "**/*.yaml" -lint:markdown: - stage: lint - image: node:lts-alpine - script: - - npm install -g markdownlint-cli - - markdownlint '**/*.md' - only: - changes: - - "**/*.md" -# Test stage -test:unit: - stage: test - image: python:3.11-slim - script: - - pip install -r requirements.txt - - pytest tests/unit - coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' - artifacts: - reports: - junit: test-results.xml - coverage_report: - coverage_format: cobertura - path: coverage.xml - paths: - - htmlcov/ - expire_in: 1 week -test:integration: - stage: test - image: python:3.11-slim - services: - - postgres:latest - variables: - POSTGRES_DB: testdb - POSTGRES_USER: testuser - POSTGRES_PASSWORD: testpass - script: - - pip install -r requirements.txt - - pytest tests/integration -# Build stage -build:podman: - stage: build - image: quay.io/podman/stable - script: - - podman build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - - podman tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest - - podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - - podman push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - - podman push $CI_REGISTRY_IMAGE:latest - only: - - main - - tags -build:binary: - stage: build - image: rust:latest - script: - - cargo build --release - artifacts: - paths: - - target/release/ - expire_in: 1 month -# Security stage -security:sast: - stage: security - image: returntocorp/semgrep - script: - - semgrep --config=auto --json --output=semgrep-report.json - artifacts: - reports: - sast: semgrep-report.json - allow_failure: true -security:dependency-scan: - stage: security - image: aquasec/trivy:latest - script: - - trivy fs --format json --output trivy-report.json . - artifacts: - reports: - dependency_scanning: trivy-report.json - allow_failure: true -security:container-scan: - stage: security - image: aquasec/trivy:latest - script: - - trivy image --format json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - only: - - main - allow_failure: true -# Deploy stage -deploy:staging: - stage: deploy - image: alpine:latest - script: - - echo "Deploying to staging..." - - apk add --no-cache curl - - curl -X POST $STAGING_WEBHOOK_URL - environment: - name: staging - url: https://staging.example.com - only: - - main -deploy:production: - stage: deploy - image: alpine:latest - script: - - echo "Deploying to production..." - - apk add --no-cache curl - - curl -X POST $PRODUCTION_WEBHOOK_URL - environment: - name: production - url: https://example.com - only: - - tags - when: manual -trufflehog: - stage: security - image: trufflesecurity/trufflehog:latest - script: - - trufflehog git file://. --only-verified --fail diff --git a/czech-file-knife/.machine_readable/AGENTIC.scm b/czech-file-knife/.machine_readable/AGENTIC.scm deleted file mode 100644 index adfd5124c..000000000 --- a/czech-file-knife/.machine_readable/AGENTIC.scm +++ /dev/null @@ -1,16 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; AGENTIC.scm - AI agent interaction patterns for czech-file-knife - -(define agentic-config - `((version . "1.0.0") - (claude-code - ((model . "claude-opus-4-5-20251101") - (tools . ("read" "edit" "bash" "grep" "glob")) - (permissions . "read-all"))) - (patterns - ((code-review . "thorough") - (refactoring . "conservative") - (testing . "comprehensive"))) - (constraints - ((languages . ()) - (banned . ("typescript" "go" "python" "makefile")))))) diff --git a/czech-file-knife/.machine_readable/ECOSYSTEM.scm b/czech-file-knife/.machine_readable/ECOSYSTEM.scm deleted file mode 100644 index 23256f3b3..000000000 --- a/czech-file-knife/.machine_readable/ECOSYSTEM.scm +++ /dev/null @@ -1,20 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; ECOSYSTEM.scm - Ecosystem position for czech-file-knife -;; Media-Type: application/vnd.ecosystem+scm - -(ecosystem - (version "1.0") - (name "czech-file-knife") - (type "") - (purpose "") - - (position-in-ecosystem - (category "") - (subcategory "") - (unique-value ())) - - (related-projects ()) - - (what-this-is ()) - - (what-this-is-not ())) diff --git a/czech-file-knife/.machine_readable/META.scm b/czech-file-knife/.machine_readable/META.scm deleted file mode 100644 index 30d021f16..000000000 --- a/czech-file-knife/.machine_readable/META.scm +++ /dev/null @@ -1,17 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; META.scm - Meta-level information for czech-file-knife -;; Media-Type: application/meta+scheme - -(meta - (architecture-decisions ()) - - (development-practices - (code-style ()) - (security - (principle "Defense in depth")) - (testing ()) - (versioning "SemVer") - (documentation "AsciiDoc") - (branching "main for stable")) - - (design-rationale ())) diff --git a/czech-file-knife/.machine_readable/NEUROSYM.scm b/czech-file-knife/.machine_readable/NEUROSYM.scm deleted file mode 100644 index a03523206..000000000 --- a/czech-file-knife/.machine_readable/NEUROSYM.scm +++ /dev/null @@ -1,13 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; NEUROSYM.scm - Neurosymbolic integration config for czech-file-knife - -(define neurosym-config - `((version . "1.0.0") - (symbolic-layer - ((type . "scheme") - (reasoning . "deductive") - (verification . "formal"))) - (neural-layer - ((embeddings . false) - (fine-tuning . false))) - (integration . ()))) diff --git a/czech-file-knife/.machine_readable/PLAYBOOK.scm b/czech-file-knife/.machine_readable/PLAYBOOK.scm deleted file mode 100644 index 61d0f8e45..000000000 --- a/czech-file-knife/.machine_readable/PLAYBOOK.scm +++ /dev/null @@ -1,13 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; PLAYBOOK.scm - Operational runbook for czech-file-knife - -(define playbook - `((version . "1.0.0") - (procedures - ((deploy . (("build" . "just build") - ("test" . "just test") - ("release" . "just release"))) - (rollback . ()) - (debug . ()))) - (alerts . ()) - (contacts . ()))) diff --git a/czech-file-knife/.machine_readable/STATE.scm b/czech-file-knife/.machine_readable/STATE.scm deleted file mode 100644 index bde217b68..000000000 --- a/czech-file-knife/.machine_readable/STATE.scm +++ /dev/null @@ -1,39 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; STATE.scm - Project state for czech-file-knife -;; Media-Type: application/vnd.state+scm - -(state - (metadata - (version "0.0.1") - (schema-version "1.0") - (created "2026-01-03") - (updated "2026-02-01") - (project "czech-file-knife") - (repo "github.com/hyperpolymath/czech-file-knife")) - - (project-context - (name "czech-file-knife") - (tagline "") - (tech-stack ())) - - (current-position - (phase "initial") - (overall-completion 0) - (components ()) - (working-features ())) - - (route-to-mvp - (milestones ())) - - (blockers-and-issues - (critical) - (high) - (medium) - (low)) - - (critical-next-actions - (immediate) - (this-week) - (this-month)) - - (session-history ())) diff --git a/czech-file-knife/.nojekyll b/czech-file-knife/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/czech-file-knife/.tool-versions b/czech-file-knife/.tool-versions deleted file mode 100644 index c996d7149..000000000 --- a/czech-file-knife/.tool-versions +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# asdf/mise tool versions -# See: https://asdf-vm.com/ - -rust 1.83.0 -just 1.36.0 diff --git a/czech-file-knife/ABI-FFI-README.adoc b/czech-file-knife/ABI-FFI-README.adoc deleted file mode 100644 index 8e5244189..000000000 --- a/czech-file-knife/ABI-FFI-README.adoc +++ /dev/null @@ -1,409 +0,0 @@ -\{\{~ Aditionally delete this line and fill out the template below ~}} - -== \{\{PROJECT}} ABI/FFI Documentation - -=== Overview - -This library follows the *Hyperpolymath RSR Standard* for ABI and FFI -design: - -* *ABI (Application Binary Interface)* defined in *Idris2* with formal -proofs -* *FFI (Foreign Function Interface)* implemented in *Zig* for C -compatibility -* *Generated C headers* bridge Idris2 ABI to Zig FFI -* *Any language* can call through standard C ABI - -=== Architecture - -.... -┌─────────────────────────────────────────────┐ -│ ABI Definitions (Idris2) │ -│ src/abi/ │ -│ - Types.idr (Type definitions) │ -│ - Layout.idr (Memory layout proofs) │ -│ - Foreign.idr (FFI declarations) │ -└─────────────────┬───────────────────────────┘ - │ - │ generates (at compile time) - ▼ -┌─────────────────────────────────────────────┐ -│ C Headers (auto-generated) │ -│ generated/abi/{{project}}.h │ -└─────────────────┬───────────────────────────┘ - │ - │ imported by - ▼ -┌─────────────────────────────────────────────┐ -│ FFI Implementation (Zig) │ -│ ffi/zig/src/main.zig │ -│ - Implements C-compatible functions │ -│ - Zero-cost abstractions │ -│ - Memory-safe by default │ -└─────────────────┬───────────────────────────┘ - │ - │ compiled to lib{{project}}.so/.a - ▼ -┌─────────────────────────────────────────────┐ -│ Any Language via C ABI │ -│ - Rust, ReScript, Julia, Python, etc. │ -└─────────────────────────────────────────────┘ -.... - -=== Directory Structure - -.... -{{project}}/ -├── src/ -│ ├── abi/ # ABI definitions (Idris2) -│ │ ├── Types.idr # Core type definitions with proofs -│ │ ├── Layout.idr # Memory layout verification -│ │ └── Foreign.idr # FFI function declarations -│ └── lib/ # Core library (any language) -│ -├── ffi/ -│ └── zig/ # FFI implementation (Zig) -│ ├── build.zig # Build configuration -│ ├── build.zig.zon # Dependencies -│ ├── src/ -│ │ └── main.zig # C-compatible FFI implementation -│ ├── test/ -│ │ └── integration_test.zig -│ └── include/ -│ └── {{project}}.h # C header (optional, can be generated) -│ -├── generated/ # Auto-generated files -│ └── abi/ -│ └── {{project}}.h # Generated from Idris2 ABI -│ -└── bindings/ # Language-specific wrappers (optional) - ├── rust/ - ├── rescript/ - └── julia/ -.... - -=== Why Idris2 for ABI? - -==== 1. *Formal Verification* - -Idris2’s dependent types allow proving properties about the ABI at -compile-time: - -[source,idris] ----- --- Prove struct size is correct -public export -exampleStructSize : HasSize ExampleStruct 16 - --- Prove field alignment is correct -public export -fieldAligned : Divides 8 (offsetOf ExampleStruct.field) - --- Prove ABI is platform-compatible -public export -abiCompatible : Compatible (ABI 1) (ABI 2) ----- - -==== 2. *Type Safety* - -Encode invariants that C/Zig cannot express: - -[source,idris] ----- --- Non-null pointer guaranteed at type level -data Handle : Type where - MkHandle : (ptr : Bits64) -> {auto 0 nonNull : So (ptr /= 0)} -> Handle - --- Array with length proof -data Buffer : (n : Nat) -> Type where - MkBuffer : Vect n Byte -> Buffer n ----- - -==== 3. *Platform Abstraction* - -Platform-specific types with compile-time selection: - -[source,idris] ----- -CInt : Platform -> Type -CInt Linux = Bits32 -CInt Windows = Bits32 - -CSize : Platform -> Type -CSize Linux = Bits64 -CSize Windows = Bits64 ----- - -==== 4. *Safe Evolution* - -Prove that new ABI versions are backward-compatible: - -[source,idris] ----- --- Compiler enforces compatibility -abiUpgrade : ABI 1 -> ABI 2 -abiUpgrade old = MkABI2 { - -- Must preserve all v1 fields - v1_compat = old, - -- Can add new fields - new_features = defaults -} ----- - -=== Why Zig for FFI? - -==== 1. *C ABI Compatibility* - -Zig exports C-compatible functions naturally: - -[source,zig] ----- -export fn library_function(param: i32) i32 { - return param * 2; -} ----- - -==== 2. *Memory Safety* - -Compile-time safety without runtime overhead: - -[source,zig] ----- -// Null check enforced at compile time -const handle = init() orelse return error.InitFailed; -defer free(handle); ----- - -==== 3. *Cross-Compilation* - -Built-in cross-compilation to any platform: - -[source,bash] ----- -zig build -Dtarget=x86_64-linux -zig build -Dtarget=aarch64-macos -zig build -Dtarget=x86_64-windows ----- - -==== 4. *Zero Dependencies* - -No runtime, no libc required (unless explicitly needed): - -[source,zig] ----- -// Minimal binary size -pub const lib = @import("std"); -// Only includes what you use ----- - -=== Building - -==== Build FFI Library - -[source,bash] ----- -cd ffi/zig -zig build # Build debug -zig build -Doptimize=ReleaseFast # Build optimized -zig build test # Run tests ----- - -==== Generate C Header from Idris2 ABI - -[source,bash] ----- -cd src/abi -idris2 --cg c-header Types.idr -o ../../generated/abi/{{project}}.h ----- - -==== Cross-Compile - -[source,bash] ----- -cd ffi/zig - -# Linux x86_64 -zig build -Dtarget=x86_64-linux - -# macOS ARM64 -zig build -Dtarget=aarch64-macos - -# Windows x86_64 -zig build -Dtarget=x86_64-windows ----- - -=== Usage - -==== From C - -[source,c] ----- -#include "{{project}}.h" - -int main() { - void* handle = {{project}}_init(); - if (!handle) return 1; - - int result = {{project}}_process(handle, 42); - if (result != 0) { - const char* err = {{project}}_last_error(); - fprintf(stderr, "Error: %s\n", err); - } - - {{project}}_free(handle); - return 0; -} ----- - -Compile with: - -[source,bash] ----- -gcc -o example example.c -l{{project}} -L./zig-out/lib ----- - -==== From Idris2 - -[source,idris] ----- -import {{PROJECT}}.ABI.Foreign - -main : IO () -main = do - Just handle <- init - | Nothing => putStrLn "Failed to initialize" - - Right result <- process handle 42 - | Left err => putStrLn $ "Error: " ++ errorDescription err - - free handle - putStrLn "Success" ----- - -==== From Rust - -[source,rust] ----- -#[link(name = "{{project}}")] -extern "C" { - fn {{project}}_init() -> *mut std::ffi::c_void; - fn {{project}}_free(handle: *mut std::ffi::c_void); - fn {{project}}_process(handle: *mut std::ffi::c_void, input: u32) -> i32; -} - -fn main() { - unsafe { - let handle = {{project}}_init(); - assert!(!handle.is_null()); - - let result = {{project}}_process(handle, 42); - assert_eq!(result, 0); - - {{project}}_free(handle); - } -} ----- - -==== From Julia - -[source,julia] ----- -const lib{{project}} = "lib{{project}}" - -function init() - handle = ccall((:{{project}}_init, lib{{project}}), Ptr{Cvoid}, ()) - handle == C_NULL && error("Failed to initialize") - handle -end - -function process(handle, input) - result = ccall((:{{project}}_process, lib{{project}}), Cint, (Ptr{Cvoid}, UInt32), handle, input) - result -end - -function cleanup(handle) - ccall((:{{project}}_free, lib{{project}}), Cvoid, (Ptr{Cvoid},), handle) -end - -# Usage -handle = init() -try - result = process(handle, 42) - println("Result: $result") -finally - cleanup(handle) -end ----- - -=== Testing - -==== Unit Tests (Zig) - -[source,bash] ----- -cd ffi/zig -zig build test ----- - -==== Integration Tests - -[source,bash] ----- -cd ffi/zig -zig build test-integration ----- - -==== ABI Verification (Idris2) - -[source,idris] ----- --- Compile-time verification -%runElab verifyABI - --- Runtime checks -main : IO () -main = do - verifyLayoutsCorrect - verifyAlignmentsCorrect - putStrLn "ABI verification passed" ----- - -=== Contributing - -When modifying the ABI/FFI: - -[arabic] -. *Update ABI first* (`+src/abi/*.idr+`) -* Modify type definitions -* Update proofs -* Ensure backward compatibility -. *Generate C header* -+ -[source,bash] ----- -idris2 --cg c-header src/abi/Types.idr -o generated/abi/{{project}}.h ----- -. *Update FFI implementation* (`+ffi/zig/src/main.zig+`) -* Implement new functions -* Match ABI types exactly -. *Add tests* -* Unit tests in Zig -* Integration tests -* ABI verification tests -. *Update documentation* -* Function signatures -* Usage examples -* Migration guide (if breaking changes) - -=== License - -\{\{LICENSE}} - -=== See Also - -* https://idris2.readthedocs.io[Idris2 Documentation] -* https://ziglang.org/documentation/master/[Zig Documentation] -* https://github.com/hyperpolymath/rhodium-standard-repositories[Rhodium -Standard Repositories] -* link:../ffi-migration-guide.md[FFI Migration Guide] -* link:../abi-migration-guide.md[ABI Migration Guide] diff --git a/czech-file-knife/AI.a2ml b/czech-file-knife/AI.a2ml deleted file mode 100644 index c676bae16..000000000 --- a/czech-file-knife/AI.a2ml +++ /dev/null @@ -1,16 +0,0 @@ -name = "czech-file-knife" - -# AI Assistant Instructions - -## Repository Focus -- `rsr-template-repo` is treated as a Rhodium Standard Repository; obey the Rhodium policies, maintain `.bot_directives`, and keep `.machines_readable/6scm/` authoritative. -- Prefer to keep generated files out of source control, and regenerate them with the documented commands before committing. - -## Workflow -1. Inspect `.machines_readable/6scm/STATE.scm` for blockers and next actions. -2. Respect any constraints listed inside `.machines_readable/6scm/AGENTIC.scm` when tooling changes are requested. -3. After finishing edits, update STATE with your outcomes and commit with a concise, imperative message. - -## Delivery Promises -- Mention in summaries whether STATE, `contractiles/`, or `.bot_directives/` changed. -- Keep this file in sync with the repository’s status; update it if the governance changes. diff --git a/czech-file-knife/AI.djot b/czech-file-knife/AI.djot deleted file mode 100644 index 2176310de..000000000 --- a/czech-file-knife/AI.djot +++ /dev/null @@ -1,100 +0,0 @@ -# AI Assistant Specification - -## Machine-Readable Artefacts - -The `.machine_readable/` directory contains structured metadata required for automated reasoning and project introspection. - -- `STATE.scm`: Current project state, milestones, and progress. -- `META.scm`: Architecture decisions, development methodology, and rationale. -- `ECOSYSTEM.scm`: Ecosystem placement, upstream/downstream relationships, and interoperability. -- `AGENTIC.scm`: AI agent interaction patterns, affordances, and constraints. -- `NEUROSYM.scm`: Neurosymbolic integration configuration and hybrid reasoning settings. -- `PLAYBOOK.scm`: Operational runbook, workflows, and incident procedures. - -Additional artefact: - -- `AI.djot`: AI Assistant behavioural and operational instructions. - -## Repository Roots - -- Local repository root: `/var/mnt/eclipse/repos` -- Remote mapping: `https://github.com/hyperpolymath/` -- Repository names are lowercase; dots (`.`) are replaced with hyphens (`-`). -- If a repository is missing locally, check the remote and synchronize before proceeding. - -## AI CLI Standards - -- Include `ai-cli-crash-capture/` and `contractiles/` from this template in all new repositories. -- Mirror all 6SCM files into `.machine_readable/6scm/`. -- Before generating code, inspect `/var/mnt/eclipse/repos/proven` for the current Idris “unbreakable” library status. -- If the project depends on the unbreakable Idris library, add an *Idris Inside* badge to the repository. -- When new bindings are required, create them under `/var/mnt/eclipse/repos/proven` using the naming pattern `idris2-zig-`. -- If new code implies additional unbreakable Idris modules, add them to `/var/mnt/eclipse/repos/proven` and update binders across all supported languages. - -## Language Policy - -### Allowed Languages and Tools - -| Language/Tool | Use Case | Notes | -|---------------|----------|-------| -| ReScript | Application code | Type-safe, compiles to JS | -| Deno | Runtime and package management | Replaces Node/npm/bun | -| Rust | Systems, performance-critical, WASM | Preferred for CLI tools | -| Tauri 2.0+ | Mobile apps (iOS/Android) | Rust backend + web UI | -| Dioxus | Mobile apps (native UI) | Pure Rust, React-like | -| Gleam | Backend services | BEAM or JS targets | -| Bash/POSIX Shell | Automation scripts | Keep minimal | -| JavaScript | Only where ReScript cannot | MCP glue, Deno APIs | -| Nickel | Configuration | Complex config logic | -| Guile Scheme | Metadata/state files | For all `.scm` artefacts | -| Julia | Batch/data processing | Per RSR | -| OCaml | AffineScript compiler | Language-specific | -| Ada | Safety-critical systems | Where required | - -### Banned Languages and Tools - -| Banned | Replacement | -|--------|-------------| -| TypeScript | ReScript | -| Node.js | Deno | -| npm | Deno | -| Bun | Deno | -| pnpm/yarn | Deno | -| Go | Rust | -| Python | Julia / Rust / ReScript | -| Java/Kotlin | Rust / Tauri / Dioxus | -| Swift | Tauri / Dioxus | -| React Native | Tauri / Dioxus | -| Flutter/Dart | Tauri / Dioxus | - -## Mobile Development Policy - -A strict Rust-first approach applies. - -- Use **Tauri 2.0+** for web UI (ReScript) + Rust backend. -- Use **Dioxus** for pure Rust native UI. - -Both are FOSS with independent governance. - -## Enforcement Rules - -- Do not create new TypeScript files; convert existing TS to ReScript. -- Do not use `package.json` for runtime dependencies; use `deno.json` imports. -- Do not include `node_modules` in production; Deno handles dependency caching. -- Do not use Go; use Rust. -- Do not use Python; use Julia (data/batch), Rust (systems), or ReScript (apps). -- Do not use Kotlin/Swift; use Tauri 2.0+ or Dioxus for mobile. - -## Package Management - -- Primary: Guix (`guix.scm`) -- Fallback: Nix (`flake.nix`) -- JavaScript dependencies: Deno (`deno.json` imports) - -## Security Requirements - -- Do not use MD5 or SHA-1; use SHA-256 or stronger. -- Use HTTPS only. -- Do not hardcode secrets. -- Pin all dependencies by SHA. -- Include SPDX license headers in all files. diff --git a/czech-file-knife/CODE_OF_CONDUCT.adoc b/czech-file-knife/CODE_OF_CONDUCT.adoc deleted file mode 100644 index a6003933d..000000000 --- a/czech-file-knife/CODE_OF_CONDUCT.adoc +++ /dev/null @@ -1,39 +0,0 @@ -== Contributor Covenant Code of Conduct - -=== Our Pledge - -We as members, contributors, and leaders pledge to make participation in -our community a harassment-free experience for everyone, regardless of -age, body size, visible or invisible disability, ethnicity, sex -characteristics, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, -race, religion, or sexual identity and orientation. - -=== Our Standards - -Examples of behavior that contributes to a positive environment include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our -mistakes - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery -* Trolling, insulting or derogatory comments, and personal or political -attacks -* Public or private harassment -* Publishing others’ private information without explicit permission - -=== Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may -be reported to the project maintainers. All complaints will be reviewed -and investigated promptly and fairly. - -=== Attribution - -This Code of Conduct is adapted from the -https://www.contributor-covenant.org[Contributor Covenant], version 2.1. diff --git a/czech-file-knife/CONTRIBUTING.adoc b/czech-file-knife/CONTRIBUTING.adoc deleted file mode 100644 index 205642748..000000000 --- a/czech-file-knife/CONTRIBUTING.adoc +++ /dev/null @@ -1,108 +0,0 @@ -== Clone the repository - -git clone https://\{\{FORGE}}/\{\{OWNER}}/\{\{REPO}}.git cd \{\{REPO}} - -== Using Nix (recommended for reproducibility) - -nix develop - -== Or using toolbox/distrobox - -toolbox create \{\{REPO}}-dev toolbox enter \{\{REPO}}-dev # Install -dependencies manually - -== Verify setup - -just check # or: cargo check / mix compile / etc. just test # Run test -suite - -.... - -### Repository Structure -.... - -\{\{REPO}}/ ├── src/ # Source code (Perimeter 1-2) ├── lib/ # Library -code (Perimeter 1-2) ├── extensions/ # Extensions (Perimeter 2) ├── -plugins/ # Plugins (Perimeter 2) ├── tools/ # Tooling (Perimeter 2) ├── -docs/ # Documentation (Perimeter 3) │ ├── architecture/ # ADRs, specs -(Perimeter 2) │ └── proposals/ # RFCs (Perimeter 3) ├── examples/ # -Examples (Perimeter 3) ├── spec/ # Spec tests (Perimeter 3) ├── tests/ # -Test suite (Perimeter 2-3) ├── .well-known/ # Protocol files (Perimeter -1-3) ├── .github/ # GitHub config (Perimeter 1) │ ├── ISSUE_TEMPLATE/ │ -└── workflows/ ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── -CONTRIBUTING.md # This file ├── GOVERNANCE.md ├── LICENSE ├── -MAINTAINERS.md ├── README.adoc ├── SECURITY.md ├── flake.nix # Nix flake -(Perimeter 1) └── Justfile # Task runner (Perimeter 1) - -.... - ---- - -## How to Contribute - -### Reporting Bugs - -**Before reporting**: -1. Search existing issues -2. Check if it's already fixed in `{{MAIN_BRANCH}}` -3. Determine which perimeter the bug affects - -**When reporting**: - -Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - -- Clear, descriptive title -- Environment details (OS, versions, toolchain) -- Steps to reproduce -- Expected vs actual behaviour -- Logs, screenshots, or minimal reproduction - -### Suggesting Features - -**Before suggesting**: -1. Check the [roadmap](ROADMAP.md) if available -2. Search existing issues and discussions -3. Consider which perimeter the feature belongs to - -**When suggesting**: - -Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: - -- Problem statement (what pain point does this solve?) -- Proposed solution -- Alternatives considered -- Which perimeter this affects - -### Your First Contribution - -Look for issues labelled: - -- [`good first issue`](https://{{FORGE}}/{{OWNER}}/{{REPO}}/labels/good%20first%20issue) — Simple Perimeter 3 tasks -- [`help wanted`](https://{{FORGE}}/{{OWNER}}/{{REPO}}/labels/help%20wanted) — Community help needed -- [`documentation`](https://{{FORGE}}/{{OWNER}}/{{REPO}}/labels/documentation) — Docs improvements -- [`perimeter-3`](https://{{FORGE}}/{{OWNER}}/{{REPO}}/labels/perimeter-3) — Community sandbox scope - ---- - -## Development Workflow - -### Branch Naming -.... - -docs/short-description # Documentation (P3) test/what-added # Test -additions (P3) feat/short-description # New features (P2) -fix/issue-number-description # Bug fixes (P2) refactor/what-changed # -Code improvements (P2) security/what-fixed # Security fixes (P1-2) - -.... - -### Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): -.... - -(): - -{empty}[optional body] - -{empty}[optional footer] diff --git a/czech-file-knife/Cargo.lock b/czech-file-knife/Cargo.lock deleted file mode 100644 index f5972a90a..000000000 --- a/czech-file-knife/Cargo.lock +++ /dev/null @@ -1,6638 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "addr" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93b8a41dbe230ad5087cc721f8d41611de654542180586b315d9f4cf6b72bef" -dependencies = [ - "psl-types", -] - -[[package]] -name = "affinitypool" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dde2a385b82232b559baeec740c37809051c596f9b56e7da0d0da2c8e8f54f6" -dependencies = [ - "async-channel", - "num_cpus", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "ammonia" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" -dependencies = [ - "cssparser", - "html5ever", - "maplit", - "tendril", - "url", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "any_ascii" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c6333e01ba7235575b6ab53e5af10f1c327927fd97c36462917e289557ea64" - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", -] - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "arcstr" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.2.17", - "password-hash", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" -dependencies = [ - "generic-array 0.12.4", - "generic-array 0.13.3", - "generic-array 0.14.7", - "stable_deref_trait", -] - -[[package]] -name = "ascii-canvas" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" -dependencies = [ - "term", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-graphql" -version = "7.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" -dependencies = [ - "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", - "async-stream", - "async-trait", - "base64 0.22.1", - "bytes", - "fnv", - "futures-timer", - "futures-util", - "http", - "indexmap 2.12.1", - "mime", - "multer", - "num-traits", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "static_assertions_next", - "thiserror 1.0.69", -] - -[[package]] -name = "async-graphql-derive" -version = "7.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" -dependencies = [ - "Inflector", - "async-graphql-parser", - "darling 0.20.11", - "proc-macro-crate", - "proc-macro2", - "quote", - "strum", - "syn 2.0.111", - "thiserror 1.0.69", -] - -[[package]] -name = "async-graphql-parser" -version = "7.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" -dependencies = [ - "async-graphql-value", - "pest", - "serde", - "serde_json", -] - -[[package]] -name = "async-graphql-value" -version = "7.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" -dependencies = [ - "bytes", - "indexmap 2.12.1", - "serde", - "serde_json", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" - -[[package]] -name = "bcrypt" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" -dependencies = [ - "base64 0.22.1", - "blowfish", - "getrandom 0.2.16", - "subtle", - "zeroize", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.111", -] - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitpacking" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" -dependencies = [ - "crunchy", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "blowfish" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" -dependencies = [ - "byteorder", - "cipher", -] - -[[package]] -name = "bon" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" -dependencies = [ - "darling 0.21.3", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.111", -] - -[[package]] -name = "borsh" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "bytesize" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cedar-policy" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d91e3b10a0f7f2911774d5e49713c4d25753466f9e11d1cd2ec627f8a2dc857" -dependencies = [ - "cedar-policy-core", - "cedar-policy-validator", - "itertools 0.10.5", - "lalrpop-util", - "ref-cast", - "serde", - "serde_json", - "smol_str", - "thiserror 1.0.69", -] - -[[package]] -name = "cedar-policy-core" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2315591c6b7e18f8038f0a0529f254235fd902b6c217aabc04f2459b0d9995" -dependencies = [ - "either", - "ipnet", - "itertools 0.10.5", - "lalrpop", - "lalrpop-util", - "lazy_static", - "miette", - "regex", - "rustc_lexer", - "serde", - "serde_json", - "serde_with", - "smol_str", - "stacker", - "thiserror 1.0.69", -] - -[[package]] -name = "cedar-policy-validator" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e756e1b2a5da742ed97e65199ad6d0893e9aa4bd6b34be1de9e70bd1e6adc7df" -dependencies = [ - "cedar-policy-core", - "itertools 0.10.5", - "serde", - "serde_json", - "serde_with", - "smol_str", - "stacker", - "thiserror 1.0.69", - "unicode-security", -] - -[[package]] -name = "census" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cfk-cache" -version = "0.1.0" -dependencies = [ - "async-trait", - "blake3", - "bytes", - "cfk-core", - "chrono", - "directories", - "heed", - "hex", - "lz4_flex 0.13.1", - "redb", - "redis", - "serde", - "serde_json", - "sled", - "surrealdb", - "thiserror 2.0.17", - "tokio", - "tracing", -] - -[[package]] -name = "cfk-cli" -version = "0.1.0" -dependencies = [ - "bytes", - "bytesize", - "cfk-core", - "cfk-providers", - "chrono", - "clap", - "console", - "futures", - "indicatif", - "tabled", - "tokio", -] - -[[package]] -name = "cfk-core" -version = "0.1.0" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "futures", - "serde", - "thiserror 2.0.17", - "tokio", -] - -[[package]] -name = "cfk-integrations" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "regex", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", -] - -[[package]] -name = "cfk-ios" -version = "0.1.0" -dependencies = [ - "async-trait", - "block", - "bytes", - "cfk-cache", - "cfk-core", - "cfk-providers", - "chrono", - "futures", - "libc", - "objc", - "objc-foundation", - "once_cell", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "cfk-providers" -version = "0.1.0" -dependencies = [ - "async-trait", - "blake3", - "bytes", - "cfk-core", - "chrono", - "futures", - "libc", - "oauth2", - "reqwest 0.13.1", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.17", - "tokio", - "tracing", -] - -[[package]] -name = "cfk-search" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "chrono", - "regex", - "serde", - "tantivy", - "thiserror 2.0.17", - "tokio", -] - -[[package]] -name = "cfk-vfs" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "cfk-providers", - "dashmap 6.1.0", - "fuser", - "parking_lot 0.12.5", - "thiserror 2.0.17", - "tokio", - "tracing", -] - -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core 0.10.1", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "console" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width 0.2.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array 0.14.7", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.111", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.111", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.111", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "dmp" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2dfc7a18dffd3ef60a442b72a827126f1557d914620f8fc4d1049916da43c1" -dependencies = [ - "trice", - "urlencoding", -] - -[[package]] -name = "double-ended-peekable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" - -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - -[[package]] -name = "doxygen-rs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" -dependencies = [ - "phf", -] - -[[package]] -name = "dtoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "earcutr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" -dependencies = [ - "itertools 0.11.0", - "num-traits", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "ena" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" -dependencies = [ - "log", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "ext-sort" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5d3b056bcc471d38082b8c453acb6670f7327fd44219b3c411e40834883569" -dependencies = [ - "log", - "rayon", - "rmp-serde", - "serde", - "tempfile", -] - -[[package]] -name = "fastdivide" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fs4" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fst" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "fuser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb29a3ae32279fe3e79a958fe01899f5fb23eadccee919cf88e145b54ed9367" -dependencies = [ - "libc", - "log", - "memchr", - "nix", - "page_size", - "smallvec", - "zerocopy", -] - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "geo" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f811f663912a69249fa620dcd2a005db7254529da2d8a0b23942e81f47084501" -dependencies = [ - "earcutr", - "float_next_after", - "geo-types", - "geographiclib-rs", - "log", - "num-traits", - "robust", - "rstar 0.12.2", - "serde", - "spade", -] - -[[package]] -name = "geo-types" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" -dependencies = [ - "approx 0.5.1", - "num-traits", - "rstar 0.10.0", - "rstar 0.11.0", - "rstar 0.12.2", - "rstar 0.8.4", - "rstar 0.9.3", - "serde", -] - -[[package]] -name = "geographiclib-rs" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" -dependencies = [ - "libm", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasm-bindgen", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heapless" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" -dependencies = [ - "as-slice", - "generic-array 0.14.7", - "hash32 0.1.1", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32 0.2.1", - "rustc_version", - "spin", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32 0.3.1", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "heed" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a56c94661ddfb51aa9cdfbf102cfcc340aa69267f95ebccc4af08d7c530d393" -dependencies = [ - "bitflags 2.10.0", - "byteorder", - "heed-traits", - "heed-types", - "libc", - "lmdb-master-sys", - "once_cell", - "page_size", - "serde", - "synchronoise", - "url", -] - -[[package]] -name = "heed-traits" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" - -[[package]] -name = "heed-types" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" -dependencies = [ - "bincode", - "byteorder", - "heed-traits", - "serde", - "serde_json", -] - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever", - "match_token", -] - -[[package]] -name = "htmlescape" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.4", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "hyperloglogplus" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" -dependencies = [ - "serde", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" -dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.2", - "unit-prefix", - "web-time", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "lalrpop" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.11.0", - "lalrpop-util", - "petgraph", - "pico-args", - "regex", - "regex-syntax", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "levenshtein_automata" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" - -[[package]] -name = "lexicmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7378d131ddf24063b32cbd7e91668d183140c4b3906270635a4d633d1068ea5d" -dependencies = [ - "any_ascii", -] - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.10.0", - "libc", -] - -[[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linfa-linalg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e7562b41c8876d3367897067013bb2884cc78e6893f092ecd26b305176ac82" -dependencies = [ - "ndarray", - "num-traits", - "rand 0.8.7", - "thiserror 1.0.69", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lmdb-master-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864808e0b19fb6dd3b70ba94ee671b82fce17554cf80aeb0a155c65bb08027df" -dependencies = [ - "cc", - "doxygen-rs", - "libc", -] - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "lz4_flex" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" - -[[package]] -name = "lz4_flex" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" -dependencies = [ - "twox-hash", -] - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "measure_time" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" -dependencies = [ - "log", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "miette-derive", - "once_cell", - "thiserror 1.0.69", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - -[[package]] -name = "murmurhash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" - -[[package]] -name = "ndarray" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" -dependencies = [ - "approx 0.4.0", - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "rawpointer", -] - -[[package]] -name = "ndarray-stats" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af5a8477ac96877b5bd1fd67e0c28736c12943aba24eda92b127e036b0c8f400" -dependencies = [ - "indexmap 1.9.3", - "itertools 0.10.5", - "ndarray", - "noisy_float", - "num-integer", - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "noisy_float" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978fe6e6ebc0bf53de533cd456ca2d9de13de13856eda1518a285d7705a213af" -dependencies = [ - "num-traits", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "oauth2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" -dependencies = [ - "base64 0.21.7", - "chrono", - "getrandom 0.2.16", - "http", - "rand 0.8.7", - "reqwest 0.12.28", - "serde", - "serde_json", - "serde_path_to_error", - "sha2", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "object_store" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1be0c6c22ec0817cdc77d3842f721a17fd30ab6965001415b5402a74e6b740" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "futures", - "http", - "humantime", - "itertools 0.14.0", - "parking_lot 0.12.5", - "percent-encoding", - "thiserror 2.0.17", - "tokio", - "tracing", - "url", - "walkdir", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "oneshot" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ownedbytes" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "papergrid" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb" -dependencies = [ - "bytecount", - "fnv", - "unicode-width 0.1.14", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "path-clean" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - -[[package]] -name = "pdqselect" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.12.1", -] - -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.7", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.111", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", - "unicase", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.111", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - -[[package]] -name = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quick_cache" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55a1aa7668676bb93926cd4e9cdfe60f03bb866553bcca9112554911b6d3dc" -dependencies = [ - "ahash 0.8.12", - "equivalent", - "hashbrown 0.14.5", - "parking_lot 0.12.5", -] - -[[package]] -name = "quick_cache" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" -dependencies = [ - "ahash 0.8.12", - "equivalent", - "hashbrown 0.16.1", - "parking_lot 0.12.5", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", - "serde", -] - -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "reblessive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc4a4ea2a66a41a1152c4b3d86e8954dc087bdf33af35446e6e176db4e73c8c" - -[[package]] -name = "redb" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06" -dependencies = [ - "libc", -] - -[[package]] -name = "redis" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfe20977fe93830c0e9817a16fbf1ed1cfd8d4bba366087a1841d2c6033c251" -dependencies = [ - "arcstr", - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "socket2", - "tokio", - "tokio-util", - "url", - "xxhash-rust", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.4", -] - -[[package]] -name = "reqwest" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "revision" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c" -dependencies = [ - "revision-derive 0.10.0", -] - -[[package]] -name = "revision" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b8ee532f15b2f0811eb1a50adf10d036e14a6cdae8d99893e7f3b921cb227d" -dependencies = [ - "chrono", - "geo", - "regex", - "revision-derive 0.11.0", - "roaring", - "rust_decimal", - "uuid", -] - -[[package]] -name = "revision-derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "revision-derive" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3415e1bc838c36f9a0a2ac60c0fa0851c72297685e66592c44870d82834dfa2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rmp" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" -dependencies = [ - "byteorder", - "rmp", - "serde", -] - -[[package]] -name = "rmpv" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" -dependencies = [ - "num-traits", - "rmp", -] - -[[package]] -name = "roaring" -version = "0.10.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" -dependencies = [ - "bytemuck", - "byteorder", - "serde", -] - -[[package]] -name = "robust" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" - -[[package]] -name = "rstar" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a45c0e8804d37e4d97e55c6f258bc9ad9c5ee7b07437009dd152d764949a27c" -dependencies = [ - "heapless 0.6.1", - "num-traits", - "pdqselect", - "serde", - "smallvec", -] - -[[package]] -name = "rstar" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40f1bfe5acdab44bc63e6699c28b74f75ec43afb59f3eda01e145aff86a25fa" -dependencies = [ - "heapless 0.7.17", - "num-traits", - "serde", - "smallvec", -] - -[[package]] -name = "rstar" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f39465655a1e3d8ae79c6d9e007f4953bfc5d55297602df9dc38f9ae9f1359a" -dependencies = [ - "heapless 0.7.17", - "num-traits", - "serde", - "smallvec", -] - -[[package]] -name = "rstar" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" -dependencies = [ - "heapless 0.7.17", - "num-traits", - "serde", - "smallvec", -] - -[[package]] -name = "rstar" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" -dependencies = [ - "heapless 0.8.0", - "num-traits", - "serde", - "smallvec", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.7", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_lexer" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-content" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "indexmap 2.12.1", - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.12.1", - "schemars 0.9.0", - "schemars 1.1.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" -dependencies = [ - "libc", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "sketches-ddsketch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" -dependencies = [ - "serde", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spade" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990" -dependencies = [ - "hashbrown 0.15.5", - "num-traits", - "robust", - "smallvec", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "static_assertions_next" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" - -[[package]] -name = "storekey" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c42833834a5d23b344f71d87114e0cc9994766a5c42938f4b50e7b2aef85b2" -dependencies = [ - "byteorder", - "memchr", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot 0.12.5", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.111", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "surrealdb" -version = "2.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3429154a8b5a98ca39100ba45ef49ae046fb1d0869dff78d78a2670b1b278982" -dependencies = [ - "arrayvec", - "async-channel", - "bincode", - "chrono", - "dmp", - "futures", - "geo", - "getrandom 0.3.4", - "indexmap 2.12.1", - "path-clean", - "pharos", - "reblessive", - "reqwest 0.12.28", - "revision 0.11.0", - "ring", - "rust_decimal", - "rustls", - "rustls-pki-types", - "semver", - "serde", - "serde-content", - "serde_json", - "surrealdb-core", - "thiserror 1.0.69", - "tokio", - "tokio-tungstenite", - "tokio-util", - "tracing", - "trice", - "url", - "uuid", - "wasm-bindgen-futures", - "wasmtimer", - "ws_stream_wasm", -] - -[[package]] -name = "surrealdb-core" -version = "2.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba423d9e7e665e4c735a1d4669c3a067135e4a574edf88af215f7f2b815e70ed" -dependencies = [ - "addr", - "affinitypool", - "ahash 0.8.12", - "ammonia", - "any_ascii", - "argon2", - "async-channel", - "async-executor", - "async-graphql", - "base64 0.21.7", - "bcrypt", - "bincode", - "blake3", - "bytes", - "castaway", - "cedar-policy", - "chrono", - "ciborium", - "dashmap 5.5.3", - "deunicode", - "dmp", - "ext-sort", - "fst", - "futures", - "fuzzy-matcher", - "geo", - "geo-types", - "getrandom 0.3.4", - "hashbrown 0.14.5", - "hex", - "http", - "ipnet", - "jsonwebtoken", - "lexicmp", - "linfa-linalg", - "md-5", - "ndarray", - "ndarray-stats", - "num-traits", - "num_cpus", - "object_store", - "parking_lot 0.12.5", - "pbkdf2", - "pharos", - "phf", - "pin-project-lite", - "quick_cache 0.5.2", - "radix_trie", - "rand 0.8.7", - "rayon", - "reblessive", - "regex", - "revision 0.11.0", - "ring", - "rmpv", - "roaring", - "rust-stemmers", - "rust_decimal", - "scrypt", - "semver", - "serde", - "serde-content", - "serde_json", - "sha1", - "sha2", - "snap", - "storekey", - "strsim", - "subtle", - "surrealdb-rocksdb", - "surrealkv", - "sysinfo", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tracing", - "trice", - "ulid", - "unicase", - "url", - "uuid", - "vart 0.8.1", - "wasm-bindgen-futures", - "wasmtimer", - "ws_stream_wasm", -] - -[[package]] -name = "surrealdb-librocksdb-sys" -version = "0.17.3+10.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db194f1cf601bb6f2d0f4cbf0931bc3e5a602bac41ef2e9a87eccdfb28b7fed2" -dependencies = [ - "bindgen", - "bzip2-sys", - "cc", - "libc", - "libz-sys", - "lz4-sys", - "zstd-sys", -] - -[[package]] -name = "surrealdb-rocksdb" -version = "0.24.0-surreal.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057727f56d48825ddbe45e4e7401cda6e99d864fbc004e7474b4689a5e72c86d" -dependencies = [ - "libc", - "surrealdb-librocksdb-sys", -] - -[[package]] -name = "surrealkv" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a5041979bdff8599a1d5f6cb7365acb9a79664e2a84e5c4fddac2b3969f7d1" -dependencies = [ - "ahash 0.8.12", - "bytes", - "chrono", - "crc32fast", - "double-ended-peekable", - "getrandom 0.2.16", - "lru", - "parking_lot 0.12.5", - "quick_cache 0.6.18", - "revision 0.10.0", - "vart 0.9.3", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synchronoise" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" -dependencies = [ - "crossbeam-queue", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "sysinfo" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tabled" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e" -dependencies = [ - "papergrid", - "tabled_derive", - "unicode-width 0.1.14", -] - -[[package]] -name = "tabled_derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "tantivy" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502915c7381c5cb2d2781503962610cb880ad8f1a0ca95df1bae645d5ebf2545" -dependencies = [ - "aho-corasick", - "arc-swap", - "base64 0.22.1", - "bitpacking", - "bon", - "byteorder", - "census", - "crc32fast", - "crossbeam-channel", - "downcast-rs", - "fastdivide", - "fnv", - "fs4", - "htmlescape", - "hyperloglogplus", - "itertools 0.14.0", - "levenshtein_automata", - "log", - "lru", - "lz4_flex 0.11.6", - "measure_time", - "memmap2", - "once_cell", - "oneshot", - "rayon", - "regex", - "rust-stemmers", - "rustc-hash", - "serde", - "serde_json", - "sketches-ddsketch", - "smallvec", - "tantivy-bitpacker", - "tantivy-columnar", - "tantivy-common", - "tantivy-fst", - "tantivy-query-grammar", - "tantivy-stacker", - "tantivy-tokenizer-api", - "tempfile", - "thiserror 2.0.17", - "time", - "uuid", - "winapi", -] - -[[package]] -name = "tantivy-bitpacker" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b04eed5108d8283607da6710fe17a7663523440eaf7ea5a1a440d19a1448b6" -dependencies = [ - "bitpacking", -] - -[[package]] -name = "tantivy-columnar" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b628488ae936c83e92b5c4056833054ca56f76c0e616aee8339e24ac89119cd" -dependencies = [ - "downcast-rs", - "fastdivide", - "itertools 0.14.0", - "serde", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-sstable", - "tantivy-stacker", -] - -[[package]] -name = "tantivy-common" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f880aa7cab0c063a47b62596d10991cdd0b6e0e0575d9c5eeb298b307a25de55" -dependencies = [ - "async-trait", - "byteorder", - "ownedbytes", - "serde", - "time", -] - -[[package]] -name = "tantivy-fst" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" -dependencies = [ - "byteorder", - "regex-syntax", - "utf8-ranges", -] - -[[package]] -name = "tantivy-query-grammar" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768fccdc84d60d86235d42d7e4c33acf43c418258ff5952abf07bd7837fcd26b" -dependencies = [ - "nom", - "serde", - "serde_json", -] - -[[package]] -name = "tantivy-sstable" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8292095d1a8a2c2b36380ec455f910ab52dde516af36321af332c93f20ab7d5" -dependencies = [ - "futures-util", - "itertools 0.14.0", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-fst", - "zstd", -] - -[[package]] -name = "tantivy-stacker" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d38a379411169f0b3002c9cba61cdfe315f757e9d4f239c00c282497a0749d" -dependencies = [ - "murmurhash32", - "rand_distr", - "tantivy-common", -] - -[[package]] -name = "tantivy-tokenizer-api" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23024f6aeb25ceb1a0e27740c84bdb0fae52626737b7e9a9de6ad5aa25c7b038" -dependencies = [ - "serde", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot 0.12.5", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" -dependencies = [ - "indexmap 2.12.1", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tracing-core" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "trice" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3aaab10ae9fac0b10f392752bf56f0fd20845f39037fec931e8537b105b515a" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.8.7", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand 0.9.3", - "serde", - "web-time", -] - -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-security" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4ddba1535dd35ed8b61c52166b7155d7f4e4b8847cec6f48e71dc66d8b5e50" -dependencies = [ - "unicode-normalization", - "unicode-script", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-ranges" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vart" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87782b74f898179396e93c0efabb38de0d58d50bbd47eae00c71b3a1144dbbae" - -[[package]] -name = "vart" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1982d899e57d646498709735f16e9224cf1e8680676ad687f930cf8b5b555ae" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.111", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmtimer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.12.5", - "pin-utils", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.4", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version", - "send_wrapper", - "thiserror 2.0.17", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/czech-file-knife/Cargo.toml b/czech-file-knife/Cargo.toml deleted file mode 100644 index 8ea61d181..000000000 --- a/czech-file-knife/Cargo.toml +++ /dev/null @@ -1,92 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "cfk-core", - "cfk-providers", - "cfk-cache", - "cfk-search", - "cfk-vfs", - "cfk-cli", - "cfk-integrations", - "cfk-ios", -] - -[workspace.package] -version = "0.1.0" -edition = "2021" -rust-version = "1.75" -license = "AGPL-3.0-or-later" -repository = "https://github.com/hyperpolymath/czech-file-knife" -homepage = "https://github.com/hyperpolymath/czech-file-knife" -documentation = "https://docs.rs/cfk-core" -authors = ["hyperpolymath"] -keywords = ["file-manager", "cloud-storage", "fuse", "sync"] -categories = ["command-line-utilities", "filesystem"] - -[workspace.dependencies] -# Async runtime -tokio = { version = "1.49", features = ["full"] } -async-trait = "0.1" -futures = "0.3" - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.8" - -# HTTP & Auth -reqwest = { version = "0.13", features = ["json", "stream"] } -oauth2 = "5.0" - -# Hashing & Compression -blake3 = "1.5" -lz4_flex = "0.13" -hex = "0.4" - -# Time & Errors -chrono = { version = "0.4", features = ["serde"] } -thiserror = "2.0" - -# CLI -clap = { version = "4.4", features = ["derive"] } -indicatif = "0.18" -console = "0.16" -dialoguer = "0.11" -tabled = "0.15" -bytesize = "2.3" - -# Storage -sled = "0.34" -directories = "6.0" - -# Search -fuzzy-matcher = "0.3" -tantivy = "0.25" -glob = "0.3" -regex = "1.10" -walkdir = "2.4" -ignore = "0.4" -infer = "0.15" - -# FUSE -fuser = "0.16" -parking_lot = "0.12" -dashmap = "6.1" - -# Bytes -bytes = "1.11" - -# FFI -libc = "0.2" -once_cell = "1.19" - -# Logging -tracing = "0.1" -tracing-subscriber = "0.3" - -# Serialization formats -rmp-serde = "1.1" # MessagePack -ciborium = "0.2" # CBOR -prost = "0.12" # Protocol Buffers -capnp = "0.18" # Cap'n Proto -flatbuffers = "23.5" # FlatBuffers diff --git a/czech-file-knife/Containerfile b/czech-file-knife/Containerfile deleted file mode 100644 index 3af317f47..000000000 --- a/czech-file-knife/Containerfile +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Build stage -FROM docker.io/library/rust:1.83-slim AS builder - -WORKDIR /build - -# Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - pkg-config \ - libfuse3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Copy source -COPY . . - -# Build release binary -RUN cargo build --release -p cfk-cli - -# Runtime stage -FROM cgr.dev/chainguard/wolfi-base:latest - -LABEL org.opencontainers.image.source="https://github.com/hyperpolymath/czech-file-knife" -LABEL org.opencontainers.image.description="Czech File Knife - Universal file management tool" -LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later" - -# Install runtime dependencies -RUN apk add --no-cache fuse3 fuse3-libs ca-certificates - -# Copy binary from builder -COPY --from=builder /build/target/release/cfk /usr/local/bin/cfk - -# Create non-root user -RUN adduser -D -u 1000 cfk -USER cfk - -WORKDIR /home/cfk - -ENTRYPOINT ["/usr/local/bin/cfk"] diff --git a/czech-file-knife/Justfile b/czech-file-knife/Justfile deleted file mode 100644 index 436d5fec6..000000000 --- a/czech-file-knife/Justfile +++ /dev/null @@ -1,32 +0,0 @@ -# czech-file-knife - Development Tasks -set shell := ["bash", "-uc"] -set dotenv-load := true - -project := "czech-file-knife" - -# Show all recipes -default: - @just --list --unsorted - -# Build -build: - @echo "TODO: Add build command" - -# Test -test: - @echo "TODO: Add test command" - -# Clean -clean: - @echo "TODO: Add clean command" - -# Format -fmt: - @echo "TODO: Add format command" - -# Lint -lint: - @echo "TODO: Add lint command" - -secret-scan-trufflehog: - @command -v trufflehog >/dev/null && trufflehog filesystem . --only-verified || true diff --git a/czech-file-knife/LICENSE b/czech-file-knife/LICENSE deleted file mode 100644 index ebda51cd7..000000000 --- a/czech-file-knife/LICENSE +++ /dev/null @@ -1,408 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath) - ------------------------------------------------------------------------- -PREFERRED LICENCE: Palimpsest License (PMPL-1.0-or-later) ------------------------------------------------------------------------- - -This work is governed by the Palimpsest License (PMPL-1.0-or-later) as -its primary intended licence. PMPL-1.0-or-later extends the Mozilla -Public License 2.0 (MPL-2.0) with additional provisions for ethical use, -post-quantum cryptographic provenance, and emotional lineage protection. -The canonical PMPL text and stewardship information are maintained at: - https://github.com/hyperpolymath/palimpsest-license - ------------------------------------------------------------------------- -FALLBACK LICENCE: Mozilla Public License 2.0 (MPL-2.0) ------------------------------------------------------------------------- - -Because PMPL-1.0-or-later is not yet recognised by the Open Source -Initiative (OSI) or equivalent bodies, this work also carries MPL-2.0 -as its legally-recognised fallback licence. - -In any jurisdiction, platform, or context where PMPL-1.0-or-later is -not accepted as a valid licence, or where an OSI-approved licence is -required, this work is instead governed by the Mozilla Public License, -Version 2.0. - -MPL-2.0 was chosen as the fallback because PMPL-1.0-or-later is -explicitly based on and extends MPL-2.0; it is therefore the closest -recognised equivalent to the intended licence. - -The complete MPL-2.0 text follows below. - ------------------------------------------------------------------------- - -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under - this License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. - For the purposes of this definition, "control" means (a) the power, - direct or indirect, to cause the direction or management of such - entity, whether by contract or otherwise, or (b) ownership of more - than fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions ---------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted -under this License. No additional rights or licenses will be implied -from the distribution or licensing of Covered Software under this -License. Notwithstanding Section 2.1(b) above, no patent license is -granted by a Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence - of its Contributions. - -This License does not grant any rights in the trademarks, service -marks, or logos of any Contributor (except as may be necessary to -comply with the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License -(if permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses -granted in Section 2.1. - -3. Responsibilities --------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients - of the Executable Form how they can obtain a copy of such Source - Code Form by reasonable means in a timely manner, at a charge no - more than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and -the Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of -Covered Software. However, You may do so only on Your own behalf, and -not on behalf of any Contributor. You must make it absolutely clear -that any such warranty, support, indemnity, or liability obligation is -offered by You alone, and You hereby agree to indemnify every -Contributor for any liability incurred by such Contributor as a result -of warranty, support, indemnity or liability terms You offer. You may -include additional disclaimers of warranty and limitations of liability -specific to any jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ------------------------------------------------------ - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description -must be placed in a text file included with all distributions of the -Covered Software under this License. Except to the extent prohibited -by statute or regulation, such description must be sufficiently -detailed for a recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on -an ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is -the first time You have received notice of non-compliance with this -License from such Contributor, and You become compliant prior to 30 -days after Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a -patent infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) -which have been validly granted by You or Your distributors under this -License prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ------------------ - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ----------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in -Section 10.3, no one other than the license steward has the right to -modify or publish new versions of this License. Each version will be -given a distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to -look for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ----------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/czech-file-knife/MAINTAINERS.adoc b/czech-file-knife/MAINTAINERS.adoc deleted file mode 100644 index 48d978175..000000000 --- a/czech-file-knife/MAINTAINERS.adoc +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -= Maintainers -:toc: preamble - -This document lists the maintainers of this project and their responsibilities. - -== Current Maintainers - -[cols="2,3,2",options="header"] -|=== -| Name | Role | Contact - -| Jonathan D.A. Jewell -| Lead Maintainer -| https://github.com/hyperpolymath[@hyperpolymath] -|=== - -== Responsibilities - -Maintainers are responsible for: - -* Reviewing and merging pull requests -* Triaging issues and feature requests -* Ensuring code quality and security standards -* Managing releases and versioning -* Upholding the project's code of conduct - -== Becoming a Maintainer - -Contributors who demonstrate: - -* Consistent, high-quality contributions -* Understanding of the project's goals and standards -* Constructive participation in discussions -* Commitment to the project's long-term health - -May be invited to become maintainers at the discretion of existing maintainers. - -== Decision Making - -* Routine decisions (bug fixes, minor improvements) can be made by any maintainer -* Significant changes require discussion and consensus among maintainers -* Breaking changes or major features should be discussed in issues before implementation - -== Contact - -For questions about project governance, open an issue or contact the maintainers listed above. diff --git a/czech-file-knife/Mustfile b/czech-file-knife/Mustfile deleted file mode 100644 index 5f075413a..000000000 --- a/czech-file-knife/Mustfile +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Mustfile - hyperpolymath mandatory checks -# See: https://github.com/hyperpolymath/mustfile - -version: 1 - -checks: - - name: security - run: just lint - - name: tests - run: just test - - name: format - run: just fmt diff --git a/czech-file-knife/PALIMPSEST.adoc b/czech-file-knife/PALIMPSEST.adoc deleted file mode 100644 index e9d2df3a4..000000000 --- a/czech-file-knife/PALIMPSEST.adoc +++ /dev/null @@ -1,41 +0,0 @@ -= Palimpsest License -:toc: -:toc-placement!: - -image:https://img.shields.io/badge/License-MPL--2.0-blue.svg[License: PMPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - -toc::[] - -== Legal Status - -This project is licensed under the **Palimpsest-MPL License 1.0 (PMPL-1.0)**. -For SPDX and tooling, use **PMPL-1.0-or-later**. - -PMPL-1.0 incorporates the Mozilla Public License 2.0 by reference and adds -ethical-use, provenance, and lineage requirements. - -== What PMPL Adds - -* **Emotional Lineage** - preserve narrative intent and cultural context -* **Provenance Integrity** - retain attribution and lineage metadata -* **Ethical Use Constraints** - explicit consent for non-interpretive AI training -* **Quantum-Safe Provenance (optional)** - post-quantum signature support - -== How to Adopt - -1. Include the PMPL-1.0 license text in `LICENSE`. -2. Add SPDX headers to source files: - `SPDX-License-Identifier: CC-BY-SA-4.0` -3. Add a Palimpsest badge to your README (see `assets/badges/` and `embed/license-blocks/`). - -== Versioning - -See `VERSIONING.adoc` for the release process and the "-or-later" model. -The current legal text is PMPL-1.0. - -== References - -* `legal/README.adoc` -* `assets/badges/README.md` -* `embed/license-blocks/README.md` diff --git a/czech-file-knife/README.adoc b/czech-file-knife/README.adoc deleted file mode 100644 index 58f4ab7bb..000000000 --- a/czech-file-knife/README.adoc +++ /dev/null @@ -1,219 +0,0 @@ -= Czech File Knife -image:https://img.shields.io/badge/License-MPL_2.0-blue.svg[MPL-2.0-or-later,link="https://opensource.org/licenses/MPL-2.0"] - - - - -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - -== License & Philosophy - -This project must declare **MPL-2.0-or-later** for platform/tooling compatibility. - -Philosophy: **Palimpsest**. The Palimpsest-MPL (PMPL) text is provided in `license/PMPL-1.0.txt`, and the canonical source is the palimpsest-license repository. - - -*Cloud-native Swiss File Knife - unified interface for 20+ storage backends* - -== Overview - -Czech File Knife (CFK) is a universal file management tool that treats all storage -backends (local, cloud, distributed, exotic) as a unified filesystem. - -== Current Status (v0.1.0) - -[cols="1,1,3"] -|=== -| Component | Status | Notes - -| *cfk-core* | Stable | Core traits, types, error handling -| *cfk-cli* | Working | Basic file operations (ls, cp, mv, rm, cat, mkdir, stat) -| *cfk-providers* | Partial | Local filesystem complete; cloud backends planned -| *cfk-cache* | Stub | Trait definitions; backend implementations coming -| *cfk-search* | Stub | Basic filename search; Tantivy integration planned -| *cfk-vfs* | Stub | FUSE mounting planned for v0.2.0 -| *cfk-integrations* | Working | aria2, agrep, pandoc integrations -|=== - -== Installation - -=== From Source - -[source,bash] ----- -# Clone the repository -git clone https://github.com/hyperpolymath/czech-file-knife.git -cd czech-file-knife - -# Build -cargo build --release - -# Install (optional) -cargo install --path cfk-cli ----- - -=== Requirements - -- Rust 1.75+ (MSRV) -- Optional: libfuse3 (for FUSE mounting) - -== Quick Start - -[source,bash] ----- -# List files -cfk ls /path/to/directory -cfk ls -l -H . # Long format with human-readable sizes - -# Display file contents -cfk cat /path/to/file.txt - -# Copy files -cfk cp source.txt dest.txt -cfk cp -f source.txt dest.txt # Force overwrite - -# Move/rename files -cfk mv old.txt new.txt - -# Remove files -cfk rm file.txt -cfk rm -r directory/ # Recursive - -# Create directories -cfk mkdir new_dir -cfk mkdir -p nested/path/dir # Create parents - -# Show file info -cfk stat file.txt - -# List registered backends -cfk backends - -# Show storage info -cfk df local ----- - -== Tech Stack - -=== Languages -[cols="1,3"] -|=== -| Language | Purpose - -| *Rust* | Core engine, providers, cache, CLI -| *Ada/SPARK* | TUI with formal verification (planned) -| *Nickel* | Configuration language (planned) -|=== - -=== Storage Backends (20+) - -==== Currently Working -- Local filesystem (all OS) - -==== Planned for Future Releases -- *Network*: NFS, SMB/CIFS, SFTP, WebDAV, 9P -- *Cloud*: Dropbox, Google Drive, OneDrive, Box, S3 -- *Distributed*: IPFS, AFS, Syncthing -- *Exotic*: Gopher, Gemini, NNTP, BitTorrent, Matrix - -=== Serialization Formats -[cols="1,2"] -|=== -| Format | Use - -| *JSON* | API responses, config -| *MessagePack* | Binary JSON alternative -| *CBOR* | Compact binary -| *Protocol Buffers* | gRPC, cross-language -| *TOML* | Config files -|=== - -=== Transport Layers -- TCP, QUIC (HTTP/3), UDP -- Unix sockets, Named pipes - -=== Databases (Cache) -- sled (pure Rust embedded KV) -- SurrealDB (multi-model) -- LMDB (via heed) -- DragonflyDB (Redis-compatible) -- redb - -=== External Integrations -- aria2 (high-speed downloads) -- agrep (fuzzy search) -- pandoc (document conversion) -- tesseract (OCR) -- eza (modern ls) - -== Project Structure - -[source] ----- -czech-file-knife/ -├── cfk-core/ # Core traits, types, errors -├── cfk-providers/ # Storage backends + transports -├── cfk-cache/ # Caching layer (sled/SurrealDB/LMDB) -├── cfk-search/ # Full-text search (Tantivy) -├── cfk-vfs/ # FUSE virtual filesystem -├── cfk-cli/ # Rust CLI -├── cfk-tui/ # Ada/SPARK TUI (planned) -└── cfk-integrations/ # External tools (aria2, pandoc, etc.) ----- - -== Development - -[source,bash] ----- -# Run tests -cargo test --workspace - -# Build in release mode -cargo build --release - -# Run CLI directly -cargo run --bin cfk -- ls . - -# Check formatting -cargo fmt --check - -# Run lints -cargo clippy --workspace ----- - -== Platform Support - -[cols="1,1,1,1,1"] -|=== -| Platform | CLI | FUSE | Cloud | Status - -| Linux x86_64 | Working | Planned | Planned | Primary -| Linux ARM64 | Working | Planned | Planned | Supported -| macOS | Working | Planned | Planned | Supported -| Windows | Working | Planned | Planned | WinFsp -| RISC-V | Untested | Untested | Untested | Experimental -|=== - -== Roadmap - -=== v0.1.0 (Current) -- Core architecture and traits -- CLI with basic file operations -- Local filesystem backend -- Unit tests - -=== v0.2.0 (Planned) -- S3/S3-compatible backend -- SFTP backend -- FUSE mounting -- Tantivy search integration - -=== v0.3.0 (Future) -- Cloud backends (Dropbox, Google Drive, OneDrive) -- WebDAV backend -- TUI implementation -- Caching layer backends - -== License - -PMPL-1.0-or-later OR Palimpsest-0.8 diff --git a/czech-file-knife/ROADMAP.adoc b/czech-file-knife/ROADMAP.adoc deleted file mode 100644 index 10beba746..000000000 --- a/czech-file-knife/ROADMAP.adoc +++ /dev/null @@ -1,171 +0,0 @@ -= Czech File Knife Roadmap -:toc: -:toclevels: 3 - -== Overview - -This roadmap outlines the development plan for Czech File Knife (CFK), the cloud-native unified interface for 20+ storage backends. - -== v1.0 - Production Release (Current) - -**Target:** 2026-02 -**Status:** Active development (v0.1.0 → v1.0) - -=== Core Storage Backends -* ✅ Local filesystem -* ✅ S3-compatible (AWS, MinIO, etc.) -* ⚠️ Azure Blob Storage (in progress) -* ⚠️ Google Cloud Storage (in progress) -* ⚠️ IPFS (in progress) - -=== Core Features -* ✅ Unified filesystem interface -* ✅ Virtual path abstraction -* ✅ Async I/O with Tokio -* ✅ Type-safe backend plugins -* ✅ Error handling with context - -=== Quality & Standards -* ✅ Comprehensive test suite -* ✅ ClusterFuzzLite fuzzing -* ✅ Criterion benchmarks -* ✅ CI/CD pipeline -* ✅ Documentation - -=== Remaining for v1.0 -* [ ] Complete ROADMAP.adoc (this file) -* [ ] Add LICENSE file -* [ ] Complete Azure/GCS backends -* [ ] IPFS integration -* [ ] Performance optimization -* [ ] Security audit - -== v1.1 - Additional Backends (Next) - -**Target:** 2026-03 - -=== New Storage Backends -* Dropbox -* OneDrive -* Box -* Backblaze B2 -* Wasabi -* SFTP/SCP -* WebDAV -* FTP/FTPS - -=== Enhanced Features -* Transparent compression -* Client-side encryption -* Caching layer -* Batch operations -* Parallel transfers - -== v1.2 - Advanced Operations - -**Target:** 2026-04 - -=== Distributed Features -* Multi-backend mirroring -* Automatic failover -* Load balancing -* Geo-replication -* Consistency guarantees - -=== Performance -* Zero-copy operations -* Memory-mapped I/O -* Streaming for large files -* Connection pooling -* Smart prefetching - -=== Developer Experience -* CLI tool for manual operations -* FUSE filesystem mount -* REST API -* GraphQL interface -* Language bindings (Python, JS, Go) - -== v2.0 - Cloud-Native Architecture - -**Target:** 2026-Q3 - -=== Kubernetes Integration -* CSI driver for Kubernetes -* Operator for CRD management -* Dynamic provisioning -* Snapshot support -* Volume cloning - -=== Observability -* OpenTelemetry integration -* Prometheus metrics -* Distributed tracing -* Structured logging -* Health checks - -=== Security -* IAM integration -* RBAC -* Audit logging -* Secret management -* mTLS support - -== v2.5 - Intelligent Storage - -**Target:** 2026-Q4 - -=== AI-Powered Features -* Automatic backend selection -* Cost optimization -* Intelligent caching -* Predictive prefetching -* Anomaly detection - -=== Policy Engine -* Retention policies -* Lifecycle management -* Compliance rules -* Data classification -* Automated archival - -== v3.0 - Universal Storage Platform - -**Target:** 2027 - -=== Ecosystem -* Plugin marketplace -* Custom backend development SDK -* Integration templates -* Best practices library -* Community backends - -=== Enterprise Features -* Multi-tenancy -* Quota management -* Billing integration -* SLA monitoring -* Disaster recovery - -=== Standards -* Open storage protocol -* Interoperability guarantees -* Vendor-neutral API -* Format migration tools - -== Long-term Vision - -* Industry-standard storage abstraction -* Zero vendor lock-in -* Seamless backend migration -* Cost-optimal storage placement -* Self-healing storage infrastructure -* Integration with all major cloud providers - -== Contributing - -See link:../.github/CONTRIBUTING.md[Contributing Guidelines] for how to contribute to Czech File Knife development. - -== Versioning - -Czech File Knife follows semantic versioning (SemVer). Breaking changes will only be introduced in major version releases. diff --git a/czech-file-knife/RSR_OUTLINE.adoc b/czech-file-knife/RSR_OUTLINE.adoc deleted file mode 100644 index d43ee2dc6..000000000 --- a/czech-file-knife/RSR_OUTLINE.adoc +++ /dev/null @@ -1,218 +0,0 @@ -= RSR Template Repository - -image:[Palimpsest-MPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] image:[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] -:toc: -:sectnums: - -// Badges -image:https://img.shields.io/badge/RSR-Infrastructure-cd7f32[RSR Infrastructure] -image:https://img.shields.io/badge/Phase-Maintenance-brightgreen[Phase] -image:https://img.shields.io/badge/Guix-Primary-purple?logo=gnu[Guix] - -== Overview - -**The canonical template for RSR (Rhodium Standard Repository) projects.** - -This repository provides the standardized structure, configuration, and tooling for all 139 repos in the hyperpolymath ecosystem. Use it to: - -* Bootstrap new projects with RSR compliance -* Reference the standard directory structure -* Copy configuration templates (Justfile, STATE.scm, etc.) - -== Quick Start - -[source,bash] ----- -# Clone the template -git clone https://github.com/hyperpolymath/RSR-template-repo my-project -cd my-project - -# Remove template git history -rm -rf .git -git init - -# Customize -sed -i 's/RSR-template-repo/my-project/g' Justfile guix.scm README.adoc - -# Enter development environment -guix shell -D -f build/guix.scm - -# Validate compliance -just validate-rsr ----- - -== What's Included - -[cols="1,3"] -|=== -|File/Directory |Purpose - -|`.editorconfig` -|Editor configuration (indent, charset) - -|`.gitignore` -|Standard ignore patterns - -|`.guix-channel` -|Guix channel definition - -|`.well-known/` -|RFC-compliant metadata (security.txt, ai.txt, humans.txt) - -|`docs/` -|Documentation directory - -|`guix.scm` -|Guix package definition - -|`justfile` -|Task runner with 50+ recipes - -|`LICENSE.txt` -|AGPL + Palimpsest dual license - -|`README.adoc` -|This file - -|`RSR_COMPLIANCE.adoc` -|Compliance tracking - -|`STATE.scm` -|Project state checkpoint -|=== - -== Justfile Features - -The template Justfile provides: - -* **~10 billion recipe combinations** via matrix recipes -* **Cookbook generation**: `just cookbook` → `docs/just-cookbook.adoc` -* **Man page generation**: `just man` → `docs/man/project.1` -* **RSR validation**: `just validate-rsr` -* **STATE.scm management**: `just state-touch`, `just state-phase` -* **Container support**: `just container-build`, `just container-push` -* **CI matrix**: `just ci-matrix [stage] [depth]` - -=== Key Recipes - -[source,bash] ----- -just # Show all recipes -just help # Detailed help -just info # Project info -just combinations # Show matrix options - -just build # Build (debug) -just test # Run tests -just quality # Format + lint + test -just ci # Full CI pipeline - -just validate # RSR + STATE validation -just docs # Generate all docs -just cookbook # Generate Justfile docs - -just guix-shell # Guix dev environment -just container-build # Build container ----- - -== Directory Structure - -[source] ----- -project/ -├── .editorconfig # Editor settings -├── .gitignore # Git ignore -├── .guix-channel # Guix channel -├── .well-known/ # RFC metadata -│ ├── ai.txt -│ ├── humans.txt -│ └── security.txt -├── config/ # Nickel configs (optional) -├── docs/ # Documentation -│ ├── generated/ -│ ├── man/ -│ └── just-cookbook.adoc -├── guix.scm # Guix package -├── Justfile # Task runner -├── LICENSE.txt # Dual license -├── README.adoc # Overview -├── RSR_COMPLIANCE.adoc # Compliance -├── src/ # Source code -├── STATE.scm # State checkpoint -└── tests/ # Tests ----- - -== RSR Compliance - -=== Language Tiers - -* **Tier 1** (Gold): Rust, Elixir, Zig, Ada, Haskell, ReScript -* **Tier 2** (Silver): Nickel, Racket, Guile Scheme, Nix -* **Infrastructure**: Guix channels, derivations - -=== Required Files - -* `.editorconfig` -* `.gitignore` -* `justfile` -* `README.adoc` -* `RSR_COMPLIANCE.adoc` -* `LICENSE.txt` (AGPL + Palimpsest) -* `.well-known/security.txt` -* `.well-known/ai.txt` -* `.well-known/humans.txt` -* `guix.scm` OR `flake.nix` - -=== Prohibited - -* Python outside `salt/` directory -* TypeScript/JavaScript (use ReScript) -* CUE (use Guile/Nickel) -* `Dockerfile` (use `Containerfile`) - -== STATE.scm - -The STATE.scm file tracks project state: - -[source,scheme] ----- -(define state - `((metadata - (project . "my-project") - (updated . "2025-12-10")) - (position - (phase . implementation) ; design|implementation|testing|maintenance|archived - (maturity . beta)) ; experimental|alpha|beta|production|lts - (ecosystem - (part-of . ("RSR Framework")) - (depends-on . ())))) ----- - -== Badge Schema - -Generate badges from STATE.scm: - -[source,bash] ----- -just badges standard ----- - -See `docs/BADGE_SCHEMA.adoc` for the full badge taxonomy. - -== Ecosystem Integration - -This template is part of: - -* **STATE.scm Ecosystem**: Conversation checkpoints -* **RSR Framework**: Repository standards -* **Consent-Aware-HTTP**: .well-known compliance - -== License - -SPDX-License-Identifier: CC-BY-SA-4.0 - -== Links - -* https://github.com/hyperpolymath/elegant-STATE[elegant-STATE] - STATE.scm tooling -* https://github.com/hyperpolymath/conative-gating[conative-gating] - Policy enforcement -* https://rhodium.sh[Rhodium Standard] - RSR documentation diff --git a/czech-file-knife/SECURITY.adoc b/czech-file-knife/SECURITY.adoc deleted file mode 100644 index abb6fbb55..000000000 --- a/czech-file-knife/SECURITY.adoc +++ /dev/null @@ -1,24 +0,0 @@ -== Security Policy - -=== Reporting a Vulnerability - -If you discover a security vulnerability, please report it responsibly: - -[arabic] -. *Do not* open a public issue -. Email security concerns to the maintainer -. Include steps to reproduce the vulnerability -. Allow reasonable time for a fix before disclosure - -=== Supported Versions - -[cols=",",options="header",] -|=== -|Version |Supported -|latest |:white_check_mark: -|< latest |Best effort -|=== - -=== Security Updates - -Security patches are released as soon as possible after verification. diff --git a/czech-file-knife/TESTING-REPORT.adoc b/czech-file-knife/TESTING-REPORT.adoc deleted file mode 100644 index 3a8e4e907..000000000 --- a/czech-file-knife/TESTING-REPORT.adoc +++ /dev/null @@ -1,84 +0,0 @@ -= Testing Report: czech-file-knife -:author: Claude Code Automated Testing -:date: 2025-12-29 -:toc: - -== Summary - -[cols="1,3"] -|=== -| Status | *PASS* -| Build | Success (release) -| Tests | 8/8 passed -| Execution | All commands work correctly -|=== - -== Build Results - -=== Command -[source,bash] ----- -cargo build --release ----- - -=== Outcome -Build completed successfully in ~67 seconds. - -=== Warnings (non-blocking) -* `cfk-providers`: 2 warnings - unexpected cfg condition `feature = "ceph"` -* `cfk-cache`: 2 warnings - unused imports -* `cfk-ios`: 3 warnings - unused imports/variables - -== Test Results - -All 8 unit tests pass across all crates. - -== Functional Testing - -[cols="1,1,2"] -|=== -| Command | Status | Notes - -| `cfk --help` | PASS | Shows all available commands -| `cfk backends` | PASS | Lists local backend as available -| `cfk ls .` | PASS | Lists directory contents correctly -| `cfk cat FILE` | PASS | Displays file contents -| `cfk stat FILE` | PASS | Shows file metadata with cfk:// URI -| `cfk mkdir DIR` | PASS | Creates directories -| `cfk cp SRC DST` | PASS | Copies files correctly -| `cfk mv SRC DST` | PASS | Moves/renames files correctly -| `cfk rm FILE` | PASS | Removes files/directories -| `cfk df local` | PASS | Shows disk space (after fix) -|=== - -== Fixes Applied - -=== 1. Implemented `get_space_info` for local backend - -*File:* `cfk-providers/src/local.rs` - -*Problem:* `cfk df local` returned "Space information not available" - -*Solution:* Implemented Unix `statvfs` call to get actual disk space: - -[source,rust] ----- -#[cfg(unix)] -{ - use std::ffi::CString; - use std::mem::MaybeUninit; - // ... statvfs implementation -} ----- - -*Dependency added:* `libc.workspace = true` in `cfk-providers/Cargo.toml` - -== Recommendations - -=== Minor (optional) -1. Add `ceph` feature to `cfk-providers/Cargo.toml` or remove cfg attribute -2. Run `cargo fix` to clean up unused imports in cfk-cache, cfk-ios - -== Conclusion - -Project fully functional. All core file operations work correctly on local filesystem. diff --git a/czech-file-knife/TESTING-REPORT.scm b/czech-file-knife/TESTING-REPORT.scm deleted file mode 100644 index 137822764..000000000 --- a/czech-file-knife/TESTING-REPORT.scm +++ /dev/null @@ -1,65 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; Testing Report for czech-file-knife -;; Generated: 2025-12-29 - -(define testing-report - '((project . "czech-file-knife") - (date . "2025-12-29") - (author . "Claude Code Automated Testing") - - (summary - (status . pass) - (build . success) - (tests-passed . 8) - (tests-failed . 0) - (execution . "all-commands-functional")) - - (build-results - (command . "cargo build --release") - (duration-seconds . 67) - (outcome . success) - (warnings - ((crate . "cfk-providers") - (count . 2) - (type . "unexpected-cfg-condition")) - ((crate . "cfk-cache") - (count . 2) - (type . "unused-imports")) - ((crate . "cfk-ios") - (count . 3) - (type . "unused-imports-variables")))) - - (test-results - (command . "cargo test") - (total . 8) - (passed . 8) - (failed . 0)) - - (functional-tests - ((command . "cfk --help") (status . pass)) - ((command . "cfk backends") (status . pass)) - ((command . "cfk ls") (status . pass)) - ((command . "cfk cat") (status . pass)) - ((command . "cfk stat") (status . pass)) - ((command . "cfk mkdir") (status . pass)) - ((command . "cfk cp") (status . pass)) - ((command . "cfk mv") (status . pass)) - ((command . "cfk rm") (status . pass)) - ((command . "cfk df local") (status . pass) (note . "fixed"))) - - (fixes-applied - ((id . 1) - (file . "cfk-providers/src/local.rs") - (problem . "get_space_info returned unknown") - (solution . "Implemented Unix statvfs call") - (dependency-added . "libc"))) - - (recommendations - ((priority . minor) - (items - ("Add ceph feature to Cargo.toml or remove cfg attribute") - ("Run cargo fix to clean up unused imports")))))) - -;; Helper to check if tests passed -(define (tests-passed? report) - (eq? (assoc-ref (assoc-ref report 'summary) 'status) 'pass)) diff --git a/czech-file-knife/benches/czech_file_knife_bench.rs b/czech-file-knife/benches/czech_file_knife_bench.rs deleted file mode 100644 index 566e1ce72..000000000 --- a/czech-file-knife/benches/czech_file_knife_bench.rs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -use criterion::{black_box, criterion_group, criterion_main, Criterion}; - -fn benchmark_basic_operations(c: &mut Criterion) { - c.bench_function("czech-file-knife basic operation", |b| { - b.iter(|| { - // Add actual benchmarking code here - black_box(42) - }); - }); -} - -criterion_group!(benches, benchmark_basic_operations); -criterion_main!(benches); diff --git a/czech-file-knife/cfk-cache/Cargo.toml b/czech-file-knife/cfk-cache/Cargo.toml deleted file mode 100644 index 4e8453f04..000000000 --- a/czech-file-knife/cfk-cache/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cfk-cache" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Offline caching layer for Czech File Knife" - -[features] -default = ["sled"] -sled = ["dep:sled"] -surrealdb = ["dep:surrealdb"] -redb = ["dep:redb"] -lmdb = ["dep:heed"] -dragonfly = ["dep:redis"] - -[dependencies] -cfk-core = { path = "../cfk-core" } -async-trait.workspace = true -blake3.workspace = true -bytes.workspace = true -chrono.workspace = true -directories.workspace = true -lz4_flex.workspace = true -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tokio.workspace = true -tracing.workspace = true -hex = "0.4" - -# Database backends (choose one) -sled = { version = "0.34", optional = true } -surrealdb = { version = "2.6.1", optional = true, features = ["kv-mem", "kv-rocksdb"] } -redb = { version = "3.1", optional = true } -heed = { version = "0.22", optional = true } # LMDB wrapper -redis = { version = "1.0", optional = true, features = ["tokio-comp"] } # DragonflyDB compatible diff --git a/czech-file-knife/cfk-cache/src/blob_store.rs b/czech-file-knife/cfk-cache/src/blob_store.rs deleted file mode 100644 index 97ab22578..000000000 --- a/czech-file-knife/cfk-cache/src/blob_store.rs +++ /dev/null @@ -1,449 +0,0 @@ -//! Content-addressed blob storage -//! -//! Stores file content using BLAKE3 hashes for deduplication. - -use blake3::Hasher; -use bytes::Bytes; -use lz4_flex::{compress_prepend_size, decompress_size_prepended}; -use std::path::{Path, PathBuf}; -use tokio::fs; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -use crate::{CacheError, CacheResult}; - -/// Content identifier (BLAKE3 hash) -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ContentId(pub [u8; 32]); - -impl ContentId { - /// Create from raw bytes - pub fn from_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - /// Create from hex string - pub fn from_hex(hex: &str) -> CacheResult { - if hex.len() != 64 { - return Err(CacheError::InvalidContentId); - } - - let mut bytes = [0u8; 32]; - hex::decode_to_slice(hex, &mut bytes) - .map_err(|_| CacheError::InvalidContentId)?; - - Ok(Self(bytes)) - } - - /// Convert to hex string - pub fn to_hex(&self) -> String { - hex::encode(self.0) - } - - /// Get the storage path for this content ID - fn storage_path(&self, base: &Path) -> PathBuf { - let hex = self.to_hex(); - // Use first 2 chars as directory for sharding - base.join(&hex[0..2]).join(&hex[2..]) - } -} - -impl std::fmt::Display for ContentId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -/// Blob storage configuration -#[derive(Debug, Clone)] -pub struct BlobStoreConfig { - /// Base directory for blob storage - pub path: PathBuf, - /// Compress blobs with LZ4 - pub compress: bool, - /// Minimum size to compress (bytes) - pub compress_threshold: usize, - /// Verify content on read - pub verify_on_read: bool, -} - -impl Default for BlobStoreConfig { - fn default() -> Self { - let cache_dir = directories::ProjectDirs::from("com", "cfk", "czech-file-knife") - .map(|d| d.cache_dir().to_path_buf()) - .unwrap_or_else(|| PathBuf::from("/tmp/cfk-cache")); - - Self { - path: cache_dir.join("blobs"), - compress: true, - compress_threshold: 1024, // 1KB - verify_on_read: true, - } - } -} - -/// Content-addressed blob store -pub struct BlobStore { - config: BlobStoreConfig, -} - -impl BlobStore { - /// Create a new blob store - pub async fn new(config: BlobStoreConfig) -> CacheResult { - // Ensure base directory exists - fs::create_dir_all(&config.path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - Ok(Self { config }) - } - - /// Create with default configuration - pub async fn default_store() -> CacheResult { - Self::new(BlobStoreConfig::default()).await - } - - /// Compute content ID for data - pub fn hash(data: &[u8]) -> ContentId { - let hash = blake3::hash(data); - ContentId(*hash.as_bytes()) - } - - /// Store blob and return content ID - pub async fn put(&self, data: Bytes) -> CacheResult { - let content_id = Self::hash(&data); - let path = content_id.storage_path(&self.config.path); - - // Check if already exists - if path.exists() { - return Ok(content_id); - } - - // Create parent directory - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - } - - // Compress if enabled and above threshold - let stored_data = if self.config.compress && data.len() >= self.config.compress_threshold { - let compressed = compress_prepend_size(&data); - // Only use compressed if it's smaller - if compressed.len() < data.len() { - Bytes::from(compressed) - } else { - data - } - } else { - data - }; - - // Write atomically using temp file - let temp_path = path.with_extension("tmp"); - let mut file = fs::File::create(&temp_path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - file.write_all(&stored_data) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - file.sync_all() - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - // Rename to final path - fs::rename(&temp_path, &path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - Ok(content_id) - } - - /// Retrieve blob by content ID - pub async fn get(&self, content_id: &ContentId) -> CacheResult { - let path = content_id.storage_path(&self.config.path); - - if !path.exists() { - return Err(CacheError::NotFound(content_id.to_string())); - } - - let mut file = fs::File::open(&path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - let mut data = Vec::new(); - file.read_to_end(&mut data) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - // Try to decompress - let decompressed = match decompress_size_prepended(&data) { - Ok(d) => Bytes::from(d), - Err(_) => Bytes::from(data), // Not compressed - }; - - // Verify content if enabled - if self.config.verify_on_read { - let computed_id = Self::hash(&decompressed); - if computed_id != *content_id { - return Err(CacheError::CorruptedContent(content_id.to_string())); - } - } - - Ok(decompressed) - } - - /// Check if blob exists - pub async fn exists(&self, content_id: &ContentId) -> bool { - let path = content_id.storage_path(&self.config.path); - path.exists() - } - - /// Delete blob by content ID - pub async fn delete(&self, content_id: &ContentId) -> CacheResult<()> { - let path = content_id.storage_path(&self.config.path); - - if path.exists() { - fs::remove_file(&path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - } - - Ok(()) - } - - /// Get size of stored blob (compressed size) - pub async fn size(&self, content_id: &ContentId) -> CacheResult { - let path = content_id.storage_path(&self.config.path); - - let metadata = fs::metadata(&path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - Ok(metadata.len()) - } - - /// Get total size of blob store - pub async fn total_size(&self) -> CacheResult { - let mut total = 0u64; - - let mut entries = fs::read_dir(&self.config.path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| CacheError::Io(e.to_string()))? - { - if entry.path().is_dir() { - let mut subdir = fs::read_dir(entry.path()) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - while let Some(file) = subdir - .next_entry() - .await - .map_err(|e| CacheError::Io(e.to_string()))? - { - if let Ok(meta) = file.metadata().await { - total += meta.len(); - } - } - } - } - - Ok(total) - } - - /// List all content IDs - pub async fn list(&self) -> CacheResult> { - let mut ids = Vec::new(); - - let mut entries = fs::read_dir(&self.config.path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| CacheError::Io(e.to_string()))? - { - let dir_name = entry.file_name().to_string_lossy().to_string(); - if dir_name.len() != 2 || !entry.path().is_dir() { - continue; - } - - let mut subdir = fs::read_dir(entry.path()) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - while let Some(file) = subdir - .next_entry() - .await - .map_err(|e| CacheError::Io(e.to_string()))? - { - let file_name = file.file_name().to_string_lossy().to_string(); - let hex = format!("{}{}", dir_name, file_name); - - if let Ok(id) = ContentId::from_hex(&hex) { - ids.push(id); - } - } - } - - Ok(ids) - } - - /// Garbage collect blobs not in the provided set - pub async fn gc(&self, keep: &std::collections::HashSet) -> CacheResult { - let mut freed = 0u64; - - let all_ids = self.list().await?; - - for id in all_ids { - if !keep.contains(&id) { - if let Ok(size) = self.size(&id).await { - freed += size; - } - self.delete(&id).await?; - } - } - - Ok(freed) - } -} - -/// Streaming blob writer for large files -pub struct BlobWriter { - hasher: Hasher, - temp_path: PathBuf, - file: Option, - compress: bool, - buffer: Vec, -} - -impl BlobWriter { - /// Create a new blob writer - pub async fn new(store: &BlobStore) -> CacheResult { - let temp_path = store - .config - .path - .join(format!("upload_{}", uuid_simple())); - - let file = fs::File::create(&temp_path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - Ok(Self { - hasher: Hasher::new(), - temp_path, - file: Some(file), - compress: store.config.compress, - buffer: Vec::new(), - }) - } - - /// Write data chunk - pub async fn write(&mut self, data: &[u8]) -> CacheResult<()> { - self.hasher.update(data); - - if let Some(ref mut file) = self.file { - file.write_all(data) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - } - - Ok(()) - } - - /// Finish writing and return content ID - pub async fn finish(mut self, store: &BlobStore) -> CacheResult { - if let Some(mut file) = self.file.take() { - file.sync_all() - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - } - - let hash = self.hasher.finalize(); - let content_id = ContentId(*hash.as_bytes()); - - let final_path = content_id.storage_path(&store.config.path); - - // Create parent directory - if let Some(parent) = final_path.parent() { - fs::create_dir_all(parent) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - } - - // Compress if needed - if self.compress { - let data = fs::read(&self.temp_path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - let compressed = compress_prepend_size(&data); - if compressed.len() < data.len() { - fs::write(&final_path, &compressed) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - fs::remove_file(&self.temp_path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - return Ok(content_id); - } - } - - // Move temp file to final location - fs::rename(&self.temp_path, &final_path) - .await - .map_err(|e| CacheError::Io(e.to_string()))?; - - Ok(content_id) - } -} - -/// Generate a simple UUID-like string -fn uuid_simple() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - - let duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - - format!("{:x}{:x}", duration.as_secs(), duration.subsec_nanos()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_blob_store() { - let config = BlobStoreConfig { - path: PathBuf::from("/tmp/cfk-test-blobs"), - compress: true, - compress_threshold: 10, - verify_on_read: true, - }; - - let store = BlobStore::new(config).await.unwrap(); - - // Store data - let data = Bytes::from("Hello, World!"); - let id = store.put(data.clone()).await.unwrap(); - - // Retrieve data - let retrieved = store.get(&id).await.unwrap(); - assert_eq!(data, retrieved); - - // Check exists - assert!(store.exists(&id).await); - - // Delete - store.delete(&id).await.unwrap(); - assert!(!store.exists(&id).await); - } -} diff --git a/czech-file-knife/cfk-cache/src/lib.rs b/czech-file-knife/cfk-cache/src/lib.rs deleted file mode 100644 index 6f715002c..000000000 --- a/czech-file-knife/cfk-cache/src/lib.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Offline caching layer for Czech File Knife -//! -//! Features: -//! - Content-addressed blob storage with BLAKE3 hashing -//! - LZ4 compression for efficient storage -//! - Metadata caching with TTL support -//! - Multiple eviction policies (LRU, LFU, FIFO, etc.) -//! -//! Supports multiple backends: -//! - sled: Pure Rust embedded KV (default) -//! - SurrealDB: Multi-model with graph queries -//! - redb: Pure Rust alternative to sled -//! - LMDB: Ultra-fast memory-mapped (via heed) -//! - DragonflyDB: Redis-compatible (via redis crate) - -#![allow(dead_code)] // Placeholder structs for future implementation - -use async_trait::async_trait; -use cfk_core::{CfkResult, VirtualPath, Entry}; -use bytes::Bytes; -use thiserror::Error; - -pub mod blob_store; -pub mod metadata_cache; -pub mod policy; - -pub use blob_store::{BlobStore, BlobStoreConfig, ContentId}; -pub use metadata_cache::{MetadataCache, MetadataCacheConfig, CachedEntry}; -pub use policy::{CachePolicy, PolicyConfig, EvictionPolicy}; - -/// Cache-specific errors -#[derive(Debug, Error)] -pub enum CacheError { - #[error("I/O error: {0}")] - Io(String), - - #[error("Database error: {0}")] - Database(String), - - #[error("Serialization error: {0}")] - Serialization(String), - - #[error("Content not found: {0}")] - NotFound(String), - - #[error("Invalid content ID")] - InvalidContentId, - - #[error("Corrupted content: {0}")] - CorruptedContent(String), - - #[error("Cache full")] - CacheFull, -} - -/// Cache result type -pub type CacheResult = Result; - -/// Cache trait for different backends -#[async_trait] -pub trait CacheBackend: Send + Sync { - /// Get cached entry metadata - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult>; - - /// Store entry metadata - async fn put_metadata(&self, path: &VirtualPath, entry: &Entry) -> CfkResult<()>; - - /// Get cached file content - async fn get_content(&self, content_hash: &str) -> CfkResult>; - - /// Store file content (content-addressed) - async fn put_content(&self, data: &[u8]) -> CfkResult; - - /// Delete cached entry - async fn delete(&self, path: &VirtualPath) -> CfkResult<()>; - - /// Clear all cache - async fn clear(&self) -> CfkResult<()>; - - /// Get cache statistics - async fn stats(&self) -> CfkResult; -} - -/// Cache statistics -#[derive(Debug, Clone, Default)] -pub struct CacheStats { - pub entries: u64, - pub total_size: u64, - pub hit_count: u64, - pub miss_count: u64, -} - -impl CacheStats { - pub fn hit_rate(&self) -> f64 { - let total = self.hit_count + self.miss_count; - if total == 0 { 0.0 } else { self.hit_count as f64 / total as f64 } - } -} - -/// Content-addressed blob storage -pub mod blob { - use super::*; - use blake3::Hasher; - use lz4_flex::{compress_prepend_size, decompress_size_prepended}; - - /// Hash content using BLAKE3 - pub fn hash_content(data: &[u8]) -> String { - let mut hasher = Hasher::new(); - hasher.update(data); - hasher.finalize().to_hex().to_string() - } - - /// Compress data using LZ4 - pub fn compress(data: &[u8]) -> Vec { - compress_prepend_size(data) - } - - /// Decompress LZ4 data - pub fn decompress(data: &[u8]) -> CfkResult> { - decompress_size_prepended(data) - .map_err(|e| cfk_core::CfkError::Cache(e.to_string())) - } -} - -/// LRU eviction policy -pub mod eviction { - use std::collections::VecDeque; - - pub struct LruPolicy { - max_size: u64, - max_entries: usize, - entries: VecDeque<(String, u64)>, // (hash, size) - } - - impl LruPolicy { - pub fn new(max_size: u64, max_entries: usize) -> Self { - Self { - max_size, - max_entries, - entries: VecDeque::new(), - } - } - - pub fn access(&mut self, hash: &str, size: u64) { - // Move to front - self.entries.retain(|(h, _)| h != hash); - self.entries.push_front((hash.to_string(), size)); - } - - pub fn evict_candidates(&self, needed_space: u64) -> Vec { - let mut freed = 0u64; - let mut to_evict = Vec::new(); - - for (hash, size) in self.entries.iter().rev() { - if freed >= needed_space { - break; - } - to_evict.push(hash.clone()); - freed += size; - } - - to_evict - } - } -} - -#[cfg(feature = "sled")] -pub mod sled_backend; - -#[cfg(feature = "surrealdb")] -pub mod surreal_backend; - -#[cfg(feature = "lmdb")] -pub mod lmdb_backend; - -#[cfg(feature = "dragonfly")] -pub mod dragonfly_backend; diff --git a/czech-file-knife/cfk-cache/src/metadata_cache.rs b/czech-file-knife/cfk-cache/src/metadata_cache.rs deleted file mode 100644 index 00432bb77..000000000 --- a/czech-file-knife/cfk-cache/src/metadata_cache.rs +++ /dev/null @@ -1,534 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! File metadata caching -//! -//! Caches file and directory metadata for offline access and performance. - -use cfk_core::{Entry, EntryKind, Metadata, VirtualPath}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::blob_store::ContentId; -use crate::{CacheError, CacheResult}; - -/// Cached entry metadata -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CachedEntry { - /// Virtual path - pub path: String, - /// Backend ID - pub backend_id: String, - /// Entry kind - pub kind: CachedEntryKind, - /// File size (for files) - pub size: Option, - /// Last modified time - pub modified: Option>, - /// Created time - pub created: Option>, - /// Content hash/checksum from provider - pub checksum: Option, - /// MIME type - pub mime_type: Option, - /// Local blob content ID (if cached) - pub content_id: Option, - /// When this entry was cached - pub cached_at: DateTime, - /// When this entry expires - pub expires_at: Option>, - /// Custom metadata - #[serde(default)] - pub custom: HashMap, -} - -/// Cached entry kind -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum CachedEntryKind { - File, - Directory, - Symlink, - Unknown, -} - -impl From for CachedEntryKind { - fn from(kind: EntryKind) -> Self { - match kind { - EntryKind::File => CachedEntryKind::File, - EntryKind::Directory => CachedEntryKind::Directory, - EntryKind::Symlink => CachedEntryKind::Symlink, - EntryKind::Unknown => CachedEntryKind::Unknown, - } - } -} - -impl From for EntryKind { - fn from(kind: CachedEntryKind) -> Self { - match kind { - CachedEntryKind::File => EntryKind::File, - CachedEntryKind::Directory => EntryKind::Directory, - CachedEntryKind::Symlink => EntryKind::Symlink, - CachedEntryKind::Unknown => EntryKind::Unknown, - } - } -} - -impl CachedEntry { - /// Create from cfk_core Entry - pub fn from_entry(entry: &Entry, ttl_secs: Option) -> Self { - Self { - path: entry.path.to_string(), - backend_id: entry.path.backend.clone(), - kind: entry.kind.into(), - size: entry.metadata.size, - modified: entry.metadata.modified, - created: entry.metadata.created, - checksum: entry.metadata.content_hash.clone(), - mime_type: entry.metadata.mime_type.clone(), - content_id: None, - cached_at: Utc::now(), - expires_at: ttl_secs.map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - custom: entry.metadata.custom.clone(), - } - } - - /// Convert back to cfk_core Entry - pub fn to_entry(&self) -> Entry { - let mut metadata = Metadata::default(); - metadata.size = self.size; - metadata.modified = self.modified; - metadata.created = self.created; - metadata.content_hash = self.checksum.clone(); - metadata.mime_type = self.mime_type.clone(); - metadata.custom = self.custom.clone(); - - Entry { - path: VirtualPath::parse_uri(&self.path).unwrap_or_else(|| { - VirtualPath::new(&self.backend_id, &self.path) - }), - kind: self.kind.into(), - metadata, - } - } - - /// Check if entry is expired - pub fn is_expired(&self) -> bool { - if let Some(expires) = self.expires_at { - Utc::now() > expires - } else { - false - } - } - - /// Set content ID after caching content - pub fn with_content_id(mut self, content_id: &ContentId) -> Self { - self.content_id = Some(content_id.to_hex()); - self - } -} - -/// Cached directory listing -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CachedDirectory { - /// Directory path - pub path: String, - /// Backend ID - pub backend_id: String, - /// Child entry paths - pub children: Vec, - /// When this listing was cached - pub cached_at: DateTime, - /// When this listing expires - pub expires_at: Option>, -} - -impl CachedDirectory { - /// Create new cached directory - pub fn new(path: &VirtualPath, children: Vec, ttl_secs: Option) -> Self { - Self { - path: path.to_string(), - backend_id: path.backend.clone(), - children, - cached_at: Utc::now(), - expires_at: ttl_secs.map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - } - } - - /// Check if listing is expired - pub fn is_expired(&self) -> bool { - if let Some(expires) = self.expires_at { - Utc::now() > expires - } else { - false - } - } -} - -/// Metadata cache configuration -#[derive(Debug, Clone)] -pub struct MetadataCacheConfig { - /// Database path (sled) - pub db_path: PathBuf, - /// Default TTL for entries (seconds) - pub default_ttl: i64, - /// Maximum entries to cache - pub max_entries: usize, -} - -impl Default for MetadataCacheConfig { - fn default() -> Self { - let cache_dir = directories::ProjectDirs::from("com", "cfk", "czech-file-knife") - .map(|d| d.cache_dir().to_path_buf()) - .unwrap_or_else(|| PathBuf::from("/tmp/cfk-cache")); - - Self { - db_path: cache_dir.join("metadata.db"), - default_ttl: 3600, // 1 hour - max_entries: 100000, - } - } -} - -/// Metadata cache -pub struct MetadataCache { - config: MetadataCacheConfig, - db: sled::Db, - /// In-memory LRU cache for hot entries - memory_cache: Arc>>, -} - -impl MetadataCache { - /// Create new metadata cache - pub fn new(config: MetadataCacheConfig) -> CacheResult { - let db = sled::open(&config.db_path) - .map_err(|e| CacheError::Database(e.to_string()))?; - - let memory_cache = Arc::new(RwLock::new(lru::LruCache::new( - std::num::NonZeroUsize::new(10000).unwrap(), - ))); - - Ok(Self { - config, - db, - memory_cache, - }) - } - - /// Create with default configuration - pub fn default_cache() -> CacheResult { - Self::new(MetadataCacheConfig::default()) - } - - /// Cache entry metadata - pub async fn put_entry(&self, entry: &Entry) -> CacheResult<()> { - let cached = CachedEntry::from_entry(entry, Some(self.config.default_ttl)); - let key = entry.path.to_string(); - let value = serde_json::to_vec(&cached) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - - self.db - .insert(format!("entry:{}", key), value) - .map_err(|e| CacheError::Database(e.to_string()))?; - - // Update memory cache - self.memory_cache.write().await.put(key, cached); - - Ok(()) - } - - /// Cache entry with custom TTL - pub async fn put_entry_with_ttl(&self, entry: &Entry, ttl_secs: i64) -> CacheResult<()> { - let cached = CachedEntry::from_entry(entry, Some(ttl_secs)); - let key = entry.path.to_string(); - let value = serde_json::to_vec(&cached) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - - self.db - .insert(format!("entry:{}", key), value) - .map_err(|e| CacheError::Database(e.to_string()))?; - - self.memory_cache.write().await.put(key, cached); - - Ok(()) - } - - /// Get cached entry - pub async fn get_entry(&self, path: &VirtualPath) -> CacheResult> { - let key = path.to_string(); - - // Check memory cache first - { - let mut cache = self.memory_cache.write().await; - if let Some(entry) = cache.get(&key) { - if !entry.is_expired() { - return Ok(Some(entry.clone())); - } - } - } - - // Check database - let db_key = format!("entry:{}", key); - if let Some(data) = self.db.get(&db_key).map_err(|e| CacheError::Database(e.to_string()))? { - let cached: CachedEntry = serde_json::from_slice(&data) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - - if cached.is_expired() { - // Remove expired entry - self.db - .remove(&db_key) - .map_err(|e| CacheError::Database(e.to_string()))?; - return Ok(None); - } - - // Add to memory cache - self.memory_cache.write().await.put(key, cached.clone()); - - return Ok(Some(cached)); - } - - Ok(None) - } - - /// Cache directory listing - pub async fn put_directory(&self, path: &VirtualPath, entries: &[Entry]) -> CacheResult<()> { - let children: Vec = entries.iter().map(|e| e.path.to_string()).collect(); - let cached = CachedDirectory::new(path, children, Some(self.config.default_ttl)); - - let key = format!("dir:{}", path); - let value = serde_json::to_vec(&cached) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - - self.db - .insert(key, value) - .map_err(|e| CacheError::Database(e.to_string()))?; - - // Also cache individual entries - for entry in entries { - self.put_entry(entry).await?; - } - - Ok(()) - } - - /// Get cached directory listing - pub async fn get_directory(&self, path: &VirtualPath) -> CacheResult>> { - let key = format!("dir:{}", path); - - if let Some(data) = self.db.get(&key).map_err(|e| CacheError::Database(e.to_string()))? { - let cached: CachedDirectory = serde_json::from_slice(&data) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - - if cached.is_expired() { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - return Ok(None); - } - - // Fetch individual entries - let mut entries = Vec::new(); - for child_path in &cached.children { - let virtual_path = VirtualPath::parse_uri(child_path).unwrap_or_else(|| { - VirtualPath::new(&cached.backend_id, child_path) - }); - - if let Some(entry) = self.get_entry(&virtual_path).await? { - entries.push(entry.to_entry()); - } - } - - return Ok(Some(entries)); - } - - Ok(None) - } - - /// Invalidate entry - pub async fn invalidate(&self, path: &VirtualPath) -> CacheResult<()> { - let key = path.to_string(); - - self.db - .remove(format!("entry:{}", key)) - .map_err(|e| CacheError::Database(e.to_string()))?; - - self.memory_cache.write().await.pop(&key); - - Ok(()) - } - - /// Invalidate directory and all children - pub async fn invalidate_directory(&self, path: &VirtualPath) -> CacheResult<()> { - let prefix = format!("entry:{}:", path); - - // Remove all entries with this prefix - for result in self.db.scan_prefix(&prefix) { - if let Ok((key, _)) = result { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - } - } - - // Remove directory listing - self.db - .remove(format!("dir:{}", path)) - .map_err(|e| CacheError::Database(e.to_string()))?; - - Ok(()) - } - - /// Clear all cached data for a backend - pub async fn clear_backend(&self, backend_id: &str) -> CacheResult<()> { - let prefix = format!("entry:{}:", backend_id); - - for result in self.db.scan_prefix(&prefix) { - if let Ok((key, _)) = result { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - } - } - - let dir_prefix = format!("dir:{}:", backend_id); - for result in self.db.scan_prefix(&dir_prefix) { - if let Ok((key, _)) = result { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - } - } - - self.memory_cache.write().await.clear(); - - Ok(()) - } - - /// Clear all cached data - pub async fn clear_all(&self) -> CacheResult<()> { - self.db.clear().map_err(|e| CacheError::Database(e.to_string()))?; - self.memory_cache.write().await.clear(); - Ok(()) - } - - /// Get cache statistics - pub async fn stats(&self) -> CacheStats { - let entry_count = self.db.scan_prefix("entry:").count(); - let dir_count = self.db.scan_prefix("dir:").count(); - let memory_size = self.memory_cache.read().await.len(); - let db_size = self.db.size_on_disk().unwrap_or(0); - - CacheStats { - entry_count, - directory_count: dir_count, - memory_entries: memory_size, - disk_size_bytes: db_size, - } - } - - /// Prune expired entries - pub async fn prune_expired(&self) -> CacheResult { - let mut pruned = 0; - - for result in self.db.scan_prefix("entry:") { - if let Ok((key, value)) = result { - if let Ok(cached) = serde_json::from_slice::(&value) { - if cached.is_expired() { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - pruned += 1; - } - } - } - } - - for result in self.db.scan_prefix("dir:") { - if let Ok((key, value)) = result { - if let Ok(cached) = serde_json::from_slice::(&value) { - if cached.is_expired() { - self.db - .remove(&key) - .map_err(|e| CacheError::Database(e.to_string()))?; - pruned += 1; - } - } - } - } - - Ok(pruned) - } -} - -/// Cache statistics -#[derive(Debug, Clone, Default)] -pub struct CacheStats { - pub entry_count: usize, - pub directory_count: usize, - pub memory_entries: usize, - pub disk_size_bytes: u64, -} - -/// Simple LRU cache implementation -mod lru { - use std::collections::HashMap; - use std::hash::Hash; - use std::num::NonZeroUsize; - - pub struct LruCache { - map: HashMap, - order: Vec, - capacity: usize, - } - - impl LruCache { - pub fn new(capacity: NonZeroUsize) -> Self { - Self { - map: HashMap::new(), - order: Vec::new(), - capacity: capacity.get(), - } - } - - pub fn get(&mut self, key: &K) -> Option<&V> { - if self.map.contains_key(key) { - // Move to front - self.order.retain(|k| k != key); - self.order.push(key.clone()); - self.map.get(key) - } else { - None - } - } - - pub fn put(&mut self, key: K, value: V) { - if self.map.contains_key(&key) { - self.order.retain(|k| k != &key); - } else if self.map.len() >= self.capacity { - // Evict oldest - if let Some(oldest) = self.order.first().cloned() { - self.map.remove(&oldest); - self.order.remove(0); - } - } - - self.map.insert(key.clone(), value); - self.order.push(key); - } - - pub fn pop(&mut self, key: &K) -> Option { - self.order.retain(|k| k != key); - self.map.remove(key) - } - - pub fn len(&self) -> usize { - self.map.len() - } - - pub fn clear(&mut self) { - self.map.clear(); - self.order.clear(); - } - } -} diff --git a/czech-file-knife/cfk-cache/src/policy.rs b/czech-file-knife/cfk-cache/src/policy.rs deleted file mode 100644 index dc9ba9e02..000000000 --- a/czech-file-knife/cfk-cache/src/policy.rs +++ /dev/null @@ -1,423 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Cache eviction policies -//! -//! LRU, LFU, FIFO, and size-based eviction strategies. - -use chrono::{DateTime, Utc}; -use std::collections::{BinaryHeap, HashMap}; -use std::cmp::Ordering; - -use crate::blob_store::ContentId; - -/// Cache entry info for eviction decisions -#[derive(Debug, Clone)] -pub struct CacheEntryInfo { - pub content_id: ContentId, - pub size: u64, - pub last_accessed: DateTime, - pub access_count: u64, - pub created: DateTime, - /// Priority (higher = more important) - pub priority: i32, -} - -impl CacheEntryInfo { - pub fn new(content_id: ContentId, size: u64) -> Self { - let now = Utc::now(); - Self { - content_id, - size, - last_accessed: now, - access_count: 1, - created: now, - priority: 0, - } - } - - pub fn touch(&mut self) { - self.last_accessed = Utc::now(); - self.access_count += 1; - } - - pub fn with_priority(mut self, priority: i32) -> Self { - self.priority = priority; - self - } -} - -/// Eviction policy type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvictionPolicy { - /// Least Recently Used - Lru, - /// Least Frequently Used - Lfu, - /// First In First Out - Fifo, - /// Largest First - LargestFirst, - /// Smallest First - SmallestFirst, - /// Adaptive Replacement Cache (ARC-like) - Adaptive, -} - -impl Default for EvictionPolicy { - fn default() -> Self { - Self::Lru - } -} - -/// Cache policy configuration -#[derive(Debug, Clone)] -pub struct PolicyConfig { - /// Maximum total size in bytes - pub max_size: u64, - /// Maximum number of entries - pub max_entries: usize, - /// Eviction policy - pub policy: EvictionPolicy, - /// Target utilization (0.0-1.0) after eviction - pub target_utilization: f64, - /// Minimum TTL (seconds) - don't evict entries newer than this - pub min_ttl: i64, -} - -impl Default for PolicyConfig { - fn default() -> Self { - Self { - max_size: 10 * 1024 * 1024 * 1024, // 10GB - max_entries: 100000, - policy: EvictionPolicy::Lru, - target_utilization: 0.9, - min_ttl: 60, // 1 minute - } - } -} - -/// Eviction result -#[derive(Debug, Clone)] -pub struct EvictionResult { - /// Content IDs to evict - pub evicted: Vec, - /// Total size freed - pub size_freed: u64, - /// Number of entries evicted - pub count: usize, -} - -/// Cache policy manager -pub struct CachePolicy { - config: PolicyConfig, - entries: HashMap, - total_size: u64, -} - -impl CachePolicy { - pub fn new(config: PolicyConfig) -> Self { - Self { - config, - entries: HashMap::new(), - total_size: 0, - } - } - - /// Record an entry being added to cache - pub fn record_add(&mut self, info: CacheEntryInfo) { - self.total_size += info.size; - self.entries.insert(info.content_id.clone(), info); - } - - /// Record an entry being accessed - pub fn record_access(&mut self, content_id: &ContentId) { - if let Some(entry) = self.entries.get_mut(content_id) { - entry.touch(); - } - } - - /// Record an entry being removed - pub fn record_remove(&mut self, content_id: &ContentId) { - if let Some(entry) = self.entries.remove(content_id) { - self.total_size = self.total_size.saturating_sub(entry.size); - } - } - - /// Check if eviction is needed - pub fn needs_eviction(&self) -> bool { - self.total_size > self.config.max_size || self.entries.len() > self.config.max_entries - } - - /// Get entries to evict - pub fn select_evictions(&self) -> EvictionResult { - if !self.needs_eviction() { - return EvictionResult { - evicted: vec![], - size_freed: 0, - count: 0, - }; - } - - let target_size = (self.config.max_size as f64 * self.config.target_utilization) as u64; - let size_to_free = self.total_size.saturating_sub(target_size); - - let target_entries = - (self.config.max_entries as f64 * self.config.target_utilization) as usize; - let entries_to_free = self.entries.len().saturating_sub(target_entries); - - let mut evicted = Vec::new(); - let mut size_freed = 0u64; - - // Get candidates sorted by eviction policy - let mut candidates: Vec<_> = self - .entries - .values() - .filter(|e| { - // Don't evict entries newer than min_ttl - let age = Utc::now() - .signed_duration_since(e.created) - .num_seconds(); - age >= self.config.min_ttl - }) - .collect(); - - // Sort by policy - match self.config.policy { - EvictionPolicy::Lru => { - candidates.sort_by(|a, b| a.last_accessed.cmp(&b.last_accessed)); - } - EvictionPolicy::Lfu => { - candidates.sort_by(|a, b| a.access_count.cmp(&b.access_count)); - } - EvictionPolicy::Fifo => { - candidates.sort_by(|a, b| a.created.cmp(&b.created)); - } - EvictionPolicy::LargestFirst => { - candidates.sort_by(|a, b| b.size.cmp(&a.size)); - } - EvictionPolicy::SmallestFirst => { - candidates.sort_by(|a, b| a.size.cmp(&b.size)); - } - EvictionPolicy::Adaptive => { - // ARC-like: balance between LRU and LFU - candidates.sort_by(|a, b| { - let a_score = adaptive_score(a); - let b_score = adaptive_score(b); - a_score.partial_cmp(&b_score).unwrap_or(Ordering::Equal) - }); - } - } - - // Select entries to evict - for candidate in candidates { - if size_freed >= size_to_free && evicted.len() >= entries_to_free { - break; - } - - evicted.push(candidate.content_id.clone()); - size_freed += candidate.size; - } - - let count = evicted.len(); - EvictionResult { - evicted, - size_freed, - count, - } - } - - /// Get current cache statistics - pub fn stats(&self) -> PolicyStats { - let avg_size = if self.entries.is_empty() { - 0 - } else { - self.total_size / self.entries.len() as u64 - }; - - let avg_access = if self.entries.is_empty() { - 0.0 - } else { - self.entries.values().map(|e| e.access_count).sum::() as f64 - / self.entries.len() as f64 - }; - - PolicyStats { - total_size: self.total_size, - entry_count: self.entries.len(), - max_size: self.config.max_size, - max_entries: self.config.max_entries, - utilization: self.total_size as f64 / self.config.max_size as f64, - avg_entry_size: avg_size, - avg_access_count: avg_access, - } - } - - /// Update policy configuration - pub fn set_config(&mut self, config: PolicyConfig) { - self.config = config; - } -} - -/// Calculate adaptive eviction score (lower = more likely to evict) -fn adaptive_score(entry: &CacheEntryInfo) -> f64 { - let age_hours = Utc::now() - .signed_duration_since(entry.last_accessed) - .num_hours() as f64; - - let frequency = entry.access_count as f64; - let size_penalty = (entry.size as f64).ln(); - let priority_bonus = entry.priority as f64 * 100.0; - - // Higher score = less likely to evict - frequency / (age_hours + 1.0) - size_penalty / 10.0 + priority_bonus -} - -/// Policy statistics -#[derive(Debug, Clone, Default)] -pub struct PolicyStats { - pub total_size: u64, - pub entry_count: usize, - pub max_size: u64, - pub max_entries: usize, - pub utilization: f64, - pub avg_entry_size: u64, - pub avg_access_count: f64, -} - -/// Priority queue for eviction candidates -struct EvictionCandidate { - content_id: ContentId, - score: f64, - size: u64, -} - -impl PartialEq for EvictionCandidate { - fn eq(&self, other: &Self) -> bool { - self.content_id == other.content_id - } -} - -impl Eq for EvictionCandidate {} - -impl PartialOrd for EvictionCandidate { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for EvictionCandidate { - fn cmp(&self, other: &Self) -> Ordering { - // Lower score = higher priority for eviction - other - .score - .partial_cmp(&self.score) - .unwrap_or(Ordering::Equal) - } -} - -/// Size-tiered caching strategy -pub struct TieredPolicy { - /// Hot tier (frequently accessed, small) - hot_tier: CachePolicy, - /// Warm tier (occasionally accessed) - warm_tier: CachePolicy, - /// Cold tier (rarely accessed, large) - cold_tier: CachePolicy, -} - -impl TieredPolicy { - pub fn new(total_size: u64) -> Self { - // 10% hot, 30% warm, 60% cold - let hot_size = total_size / 10; - let warm_size = (total_size * 3) / 10; - let cold_size = (total_size * 6) / 10; - - Self { - hot_tier: CachePolicy::new(PolicyConfig { - max_size: hot_size, - max_entries: 10000, - policy: EvictionPolicy::Lfu, - ..Default::default() - }), - warm_tier: CachePolicy::new(PolicyConfig { - max_size: warm_size, - max_entries: 50000, - policy: EvictionPolicy::Lru, - ..Default::default() - }), - cold_tier: CachePolicy::new(PolicyConfig { - max_size: cold_size, - max_entries: 100000, - policy: EvictionPolicy::LargestFirst, - ..Default::default() - }), - } - } - - /// Determine tier for an entry based on size and access pattern - pub fn determine_tier(&self, info: &CacheEntryInfo) -> Tier { - if info.access_count > 10 && info.size < 1024 * 1024 { - Tier::Hot - } else if info.access_count > 2 || info.size < 10 * 1024 * 1024 { - Tier::Warm - } else { - Tier::Cold - } - } - - /// Record entry in appropriate tier - pub fn record_add(&mut self, info: CacheEntryInfo) { - match self.determine_tier(&info) { - Tier::Hot => self.hot_tier.record_add(info), - Tier::Warm => self.warm_tier.record_add(info), - Tier::Cold => self.cold_tier.record_add(info), - } - } - - /// Get evictions from all tiers - pub fn select_evictions(&self) -> Vec { - vec![ - self.cold_tier.select_evictions(), - self.warm_tier.select_evictions(), - self.hot_tier.select_evictions(), - ] - } -} - -/// Cache tier -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Tier { - Hot, - Warm, - Cold, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_lru_eviction() { - let config = PolicyConfig { - max_size: 1000, - max_entries: 10, - policy: EvictionPolicy::Lru, - target_utilization: 0.8, - min_ttl: 0, - }; - - let mut policy = CachePolicy::new(config); - - // Add entries - for i in 0..15 { - let id = ContentId::from_bytes([i as u8; 32]); - let info = CacheEntryInfo::new(id, 100); - policy.record_add(info); - } - - assert!(policy.needs_eviction()); - - let result = policy.select_evictions(); - assert!(!result.evicted.is_empty()); - assert!(result.size_freed > 0); - } -} diff --git a/czech-file-knife/cfk-cache/src/sled_backend.rs b/czech-file-knife/cfk-cache/src/sled_backend.rs deleted file mode 100644 index 8f32bb9ef..000000000 --- a/czech-file-knife/cfk-cache/src/sled_backend.rs +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Sled database backend for cache storage - -use sled::Db; -use std::path::Path; - -use crate::{CacheError, CacheResult}; - -/// Sled-based storage backend -pub struct SledBackend { - db: Db, -} - -impl SledBackend { - /// Open or create a sled database at the given path - pub fn open(path: impl AsRef) -> CacheResult { - let db = sled::open(path).map_err(|e| CacheError::Database(e.to_string()))?; - Ok(Self { db }) - } - - /// Get a value by key - pub fn get(&self, key: &[u8]) -> CacheResult>> { - self.db - .get(key) - .map_err(|e| CacheError::Database(e.to_string())) - .map(|opt| opt.map(|v| v.to_vec())) - } - - /// Insert a key-value pair - pub fn insert(&self, key: &[u8], value: &[u8]) -> CacheResult<()> { - self.db - .insert(key, value) - .map_err(|e| CacheError::Database(e.to_string()))?; - Ok(()) - } - - /// Remove a key - pub fn remove(&self, key: &[u8]) -> CacheResult>> { - self.db - .remove(key) - .map_err(|e| CacheError::Database(e.to_string())) - .map(|opt| opt.map(|v| v.to_vec())) - } - - /// Flush to disk - pub fn flush(&self) -> CacheResult<()> { - self.db - .flush() - .map_err(|e| CacheError::Database(e.to_string()))?; - Ok(()) - } - - /// Iterate over all keys with a given prefix - pub fn scan_prefix(&self, prefix: &[u8]) -> impl Iterator, Vec)>> + '_ { - self.db.scan_prefix(prefix).map(|result| { - result - .map(|(k, v)| (k.to_vec(), v.to_vec())) - .map_err(|e| CacheError::Database(e.to_string())) - }) - } - - /// Clear all data - pub fn clear(&self) -> CacheResult<()> { - self.db - .clear() - .map_err(|e| CacheError::Database(e.to_string()))?; - Ok(()) - } - - /// Get database size on disk - pub fn size_on_disk(&self) -> CacheResult { - self.db - .size_on_disk() - .map_err(|e| CacheError::Database(e.to_string())) - } -} diff --git a/czech-file-knife/cfk-cli/Cargo.toml b/czech-file-knife/cfk-cli/Cargo.toml deleted file mode 100644 index b9c3b1e79..000000000 --- a/czech-file-knife/cfk-cli/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "cfk-cli" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Command-line interface for Czech File Knife" - -[[bin]] -name = "cfk" -path = "src/main.rs" - -[dependencies] -cfk-core = { path = "../cfk-core" } -cfk-providers = { path = "../cfk-providers" } - -# CLI -clap.workspace = true -console.workspace = true -indicatif.workspace = true -tabled.workspace = true -bytesize.workspace = true - -# Async -tokio.workspace = true -futures.workspace = true - -# Time -chrono.workspace = true - -# Bytes -bytes.workspace = true - -# Packaging metadata for cargo-deb -[package.metadata.deb] -maintainer = "hyperpolymath " -copyright = "2025, hyperpolymath" -license-file = ["../LICENSE", "0"] -extended-description = """ -Czech File Knife (cfk) is a universal file management toolkit with cloud -provider integration and virtual filesystem support. It provides a unified -interface for managing files across local storage and cloud providers. -""" -depends = "$auto, fuse3" -section = "utils" -priority = "optional" -assets = [ - ["target/release/cfk", "usr/bin/", "755"], - ["../README.adoc", "usr/share/doc/czech-file-knife/README.adoc", "644"], -] - -# Packaging metadata for cargo-generate-rpm -[package.metadata.generate-rpm] -assets = [ - { source = "target/release/cfk", dest = "/usr/bin/cfk", mode = "755" }, - { source = "../README.adoc", dest = "/usr/share/doc/czech-file-knife/README.adoc", mode = "644" }, -] - -[package.metadata.generate-rpm.requires] -fuse3 = "*" diff --git a/czech-file-knife/cfk-cli/src/commands.rs b/czech-file-knife/cfk-cli/src/commands.rs deleted file mode 100644 index fd5cb3898..000000000 --- a/czech-file-knife/cfk-cli/src/commands.rs +++ /dev/null @@ -1,401 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! CLI command implementations - -use cfk_core::{ - entry::EntryKind, - operations::{CopyOptions, DeleteOptions, ListOptions, MoveOptions, ReadOptions, WriteOptions}, - CfkError, CfkResult, VirtualPath, -}; -use cfk_providers::{BackendRegistry, LocalBackend}; -use chrono::{DateTime, Utc}; -use console::style; -use futures::StreamExt; -use std::path::PathBuf; -use std::sync::Arc; -use tabled::{Table, Tabled}; - -/// Initialize the backend registry with available backends -fn init_registry() -> BackendRegistry { - let mut registry = BackendRegistry::new(); - - // Register local filesystem with root as base - registry.register(Arc::new(LocalBackend::new("local", "/"))); - - // Future: register cloud backends based on config - - registry -} - -/// Parse a path string into a VirtualPath -/// Supports: -/// - cfk://backend/path - explicit URI -/// - /absolute/path - local absolute path -/// - relative/path - local relative path -fn parse_path(path: &str) -> CfkResult { - if let Some(vpath) = VirtualPath::parse_uri(path) { - return Ok(vpath); - } - - // Treat as local path - let path_buf = if path.starts_with('/') { - PathBuf::from(path) - } else { - let cwd = std::env::current_dir().map_err(|e| CfkError::Io(e))?; - cwd.join(path) - }; - - // Canonicalize if exists, otherwise use as-is - let canonical = path_buf - .canonicalize() - .unwrap_or_else(|_| path_buf.clone()); - - Ok(VirtualPath::new("local", canonical.to_string_lossy())) -} - -/// Format a timestamp for display -fn format_time(dt: Option>) -> String { - dt.map(|t| t.format("%Y-%m-%d %H:%M").to_string()) - .unwrap_or_else(|| "-".to_string()) -} - -/// Format file size -fn format_size(size: Option, human: bool) -> String { - match size { - Some(s) if human => bytesize::ByteSize(s).to_string(), - Some(s) => s.to_string(), - None => "-".to_string(), - } -} - -/// Format entry kind -fn format_kind(kind: EntryKind) -> String { - match kind { - EntryKind::Directory => style("d").cyan().to_string(), - EntryKind::File => "-".to_string(), - EntryKind::Symlink => style("l").magenta().to_string(), - EntryKind::Unknown => "?".to_string(), - } -} - -/// Format permissions -fn format_permissions(mode: Option) -> String { - match mode { - Some(m) => { - let r = if m & 0o400 != 0 { 'r' } else { '-' }; - let w = if m & 0o200 != 0 { 'w' } else { '-' }; - let x = if m & 0o100 != 0 { 'x' } else { '-' }; - let gr = if m & 0o040 != 0 { 'r' } else { '-' }; - let gw = if m & 0o020 != 0 { 'w' } else { '-' }; - let gx = if m & 0o010 != 0 { 'x' } else { '-' }; - let or = if m & 0o004 != 0 { 'r' } else { '-' }; - let ow = if m & 0o002 != 0 { 'w' } else { '-' }; - let ox = if m & 0o001 != 0 { 'x' } else { '-' }; - format!("{r}{w}{x}{gr}{gw}{gx}{or}{ow}{ox}") - } - None => "---------".to_string(), - } -} - -#[derive(Tabled)] -struct LsEntry { - #[tabled(rename = "Type")] - kind: String, - #[tabled(rename = "Permissions")] - perms: String, - #[tabled(rename = "Size")] - size: String, - #[tabled(rename = "Modified")] - modified: String, - #[tabled(rename = "Name")] - name: String, -} - -/// List directory contents -pub async fn ls(path: &str, long: bool, all: bool, human: bool, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - let vpath = parse_path(path)?; - - if verbose { - eprintln!("Listing: {}", vpath); - } - - let backend = registry.get_or_err(&vpath.backend)?; - let options = ListOptions { - include_hidden: all, - ..Default::default() - }; - - let listing = backend.list_directory(&vpath, &options).await?; - - if long { - let entries: Vec = listing - .entries - .iter() - .filter(|e| all || !e.name().map(|n| n.starts_with('.')).unwrap_or(false)) - .map(|e| LsEntry { - kind: format_kind(e.kind), - perms: format_permissions(e.metadata.permissions.map(|p| p.mode)), - size: format_size(e.metadata.size, human), - modified: format_time(e.metadata.modified), - name: e.name().unwrap_or("?").to_string(), - }) - .collect(); - - if entries.is_empty() { - println!("(empty directory)"); - } else { - let table = Table::new(entries).to_string(); - println!("{table}"); - } - } else { - let names: Vec<&str> = listing - .entries - .iter() - .filter(|e| all || !e.name().map(|n| n.starts_with('.')).unwrap_or(false)) - .filter_map(|e| e.name()) - .collect(); - - if names.is_empty() { - println!("(empty directory)"); - } else { - for name in names { - println!("{name}"); - } - } - } - - Ok(()) -} - -/// Display file contents -pub async fn cat(path: &str, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - let vpath = parse_path(path)?; - - if verbose { - eprintln!("Reading: {}", vpath); - } - - let backend = registry.get_or_err(&vpath.backend)?; - let options = ReadOptions::default(); - - let mut stream = backend.read_file(&vpath, &options).await?; - while let Some(chunk) = stream.next().await { - let bytes = chunk?; - // Write raw bytes to stdout - use std::io::Write; - std::io::stdout().write_all(&bytes).map_err(CfkError::Io)?; - } - - Ok(()) -} - -/// Copy files -pub async fn cp(source: &str, dest: &str, _recursive: bool, force: bool, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - let src_path = parse_path(source)?; - let dst_path = parse_path(dest)?; - - if verbose { - eprintln!("Copying: {} -> {}", src_path, dst_path); - } - - // Check if source and dest are on the same backend - if src_path.backend == dst_path.backend { - let backend = registry.get_or_err(&src_path.backend)?; - let options = CopyOptions { - overwrite: force, - preserve_metadata: true, - }; - backend.copy(&src_path, &dst_path, &options).await?; - } else { - // Cross-backend copy: read from source, write to dest - let src_backend = registry.get_or_err(&src_path.backend)?; - let dst_backend = registry.get_or_err(&dst_path.backend)?; - - let read_options = ReadOptions::default(); - let stream = src_backend.read_file(&src_path, &read_options).await?; - - let write_options = WriteOptions { - overwrite: force, - create_parents: true, - ..Default::default() - }; - - // Get source metadata for size hint - let src_meta = src_backend.get_metadata(&src_path).await?; - dst_backend - .write_file_stream(&dst_path, stream, src_meta.metadata.size, &write_options) - .await?; - } - - println!("Copied {} -> {}", source, dest); - Ok(()) -} - -/// Move/rename files -pub async fn mv(source: &str, dest: &str, force: bool, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - let src_path = parse_path(source)?; - let dst_path = parse_path(dest)?; - - if verbose { - eprintln!("Moving: {} -> {}", src_path, dst_path); - } - - if src_path.backend == dst_path.backend { - // Same backend: use rename - let backend = registry.get_or_err(&src_path.backend)?; - let options = MoveOptions { overwrite: force }; - backend.rename(&src_path, &dst_path, &options).await?; - } else { - // Cross-backend: copy then delete - cp(source, dest, true, force, verbose).await?; - rm(&[source.to_string()], true, true, verbose).await?; - } - - println!("Moved {} -> {}", source, dest); - Ok(()) -} - -/// Remove files or directories -pub async fn rm(paths: &[String], recursive: bool, force: bool, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - - for path in paths { - let vpath = parse_path(path)?; - - if verbose { - eprintln!("Removing: {}", vpath); - } - - let backend = registry.get_or_err(&vpath.backend)?; - let options = DeleteOptions { recursive, force }; - - backend.delete(&vpath, &options).await?; - println!("Removed {}", path); - } - - Ok(()) -} - -/// Create directories -pub async fn mkdir(paths: &[String], parents: bool, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - - for path in paths { - let vpath = parse_path(path)?; - - if verbose { - eprintln!("Creating directory: {}", vpath); - } - - let backend = registry.get_or_err(&vpath.backend)?; - - if parents { - // Create with parents - create_directory already does this - backend.create_directory(&vpath).await?; - } else { - // Check parent exists first - if let Some(parent) = vpath.parent() { - let parent_meta = backend.get_metadata(&parent).await; - if parent_meta.is_err() { - return Err(CfkError::NotFound(format!( - "Parent directory does not exist: {}", - parent - ))); - } - } - backend.create_directory(&vpath).await?; - } - - println!("Created {}", path); - } - - Ok(()) -} - -/// Show file/directory information -pub async fn stat(path: &str, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - let vpath = parse_path(path)?; - - if verbose { - eprintln!("Getting info: {}", vpath); - } - - let backend = registry.get_or_err(&vpath.backend)?; - let entry = backend.get_metadata(&vpath).await?; - - println!(" Path: {}", entry.path); - println!(" Type: {:?}", entry.kind); - - if let Some(size) = entry.metadata.size { - println!(" Size: {} ({})", size, bytesize::ByteSize(size)); - } - - if let Some(perms) = entry.metadata.permissions { - println!(" Mode: {:o} ({})", perms.mode, format_permissions(Some(perms.mode))); - } - - if let Some(modified) = entry.metadata.modified { - println!(" Modified: {}", modified); - } - - if let Some(created) = entry.metadata.created { - println!(" Created: {}", created); - } - - if let Some(hash) = &entry.metadata.content_hash { - println!(" Hash: {}", hash); - } - - Ok(()) -} - -/// List registered backends -pub async fn backends(_verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - - println!("Registered backends:"); - for id in registry.list() { - if let Some(backend) = registry.get(id) { - let available = if backend.is_available().await { - style("available").green() - } else { - style("unavailable").red() - }; - println!(" {} ({}) - {}", id, backend.display_name(), available); - } - } - - Ok(()) -} - -/// Show storage space information -pub async fn df(backend_id: &str, verbose: bool) -> CfkResult<()> { - let registry = init_registry(); - - if verbose { - eprintln!("Getting space info for: {}", backend_id); - } - - let backend = registry.get_or_err(backend_id)?; - let info = backend.get_space_info().await?; - - println!("Storage: {} ({})", backend_id, backend.display_name()); - - match (info.total, info.used, info.available) { - (Some(total), Some(used), Some(avail)) => { - let pct = (used as f64 / total as f64) * 100.0; - println!(" Total: {}", bytesize::ByteSize(total)); - println!(" Used: {} ({:.1}%)", bytesize::ByteSize(used), pct); - println!(" Available: {}", bytesize::ByteSize(avail)); - } - _ => { - println!(" Space information not available for this backend"); - } - } - - Ok(()) -} diff --git a/czech-file-knife/cfk-cli/src/main.rs b/czech-file-knife/cfk-cli/src/main.rs deleted file mode 100644 index e9295736c..000000000 --- a/czech-file-knife/cfk-cli/src/main.rs +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Czech File Knife CLI -//! -//! A cloud-native, universal file management tool. - -mod commands; - -use clap::{Parser, Subcommand}; -use std::process::ExitCode; - -#[derive(Parser)] -#[command(name = "cfk")] -#[command(author, version, about = "Czech File Knife - Universal file management", long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, - - /// Verbose output - #[arg(short, long, global = true)] - verbose: bool, -} - -#[derive(Subcommand)] -enum Commands { - /// List directory contents - #[command(alias = "dir")] - Ls { - /// Path to list (defaults to current directory) - #[arg(default_value = ".")] - path: String, - - /// Long format with details - #[arg(short, long)] - long: bool, - - /// Show all files including hidden - #[arg(short, long)] - all: bool, - - /// Human-readable sizes - #[arg(short = 'H', long)] - human: bool, - }, - - /// Display file contents - Cat { - /// File to display - path: String, - }, - - /// Copy files or directories - Cp { - /// Source path - source: String, - - /// Destination path - dest: String, - - /// Recursive copy for directories - #[arg(short, long)] - recursive: bool, - - /// Force overwrite existing files - #[arg(short, long)] - force: bool, - }, - - /// Move or rename files - Mv { - /// Source path - source: String, - - /// Destination path - dest: String, - - /// Force overwrite existing files - #[arg(short, long)] - force: bool, - }, - - /// Remove files or directories - Rm { - /// Path(s) to remove - #[arg(required = true)] - paths: Vec, - - /// Recursive removal for directories - #[arg(short, long)] - recursive: bool, - - /// Force removal without confirmation - #[arg(short, long)] - force: bool, - }, - - /// Create directories - Mkdir { - /// Directory path(s) to create - #[arg(required = true)] - paths: Vec, - - /// Create parent directories as needed - #[arg(short, long)] - parents: bool, - }, - - /// Show file or directory information - Stat { - /// Path to inspect - path: String, - }, - - /// List registered backends - Backends, - - /// Show storage space information - Df { - /// Backend to query (defaults to local) - #[arg(default_value = "local")] - backend: String, - }, -} - -#[tokio::main] -async fn main() -> ExitCode { - let cli = Cli::parse(); - - let result = match cli.command { - Commands::Ls { path, long, all, human } => { - commands::ls(&path, long, all, human, cli.verbose).await - } - Commands::Cat { path } => { - commands::cat(&path, cli.verbose).await - } - Commands::Cp { source, dest, recursive, force } => { - commands::cp(&source, &dest, recursive, force, cli.verbose).await - } - Commands::Mv { source, dest, force } => { - commands::mv(&source, &dest, force, cli.verbose).await - } - Commands::Rm { paths, recursive, force } => { - commands::rm(&paths, recursive, force, cli.verbose).await - } - Commands::Mkdir { paths, parents } => { - commands::mkdir(&paths, parents, cli.verbose).await - } - Commands::Stat { path } => { - commands::stat(&path, cli.verbose).await - } - Commands::Backends => { - commands::backends(cli.verbose).await - } - Commands::Df { backend } => { - commands::df(&backend, cli.verbose).await - } - }; - - match result { - Ok(()) => ExitCode::SUCCESS, - Err(e) => { - eprintln!("Error: {e}"); - ExitCode::FAILURE - } - } -} diff --git a/czech-file-knife/cfk-core/Cargo.toml b/czech-file-knife/cfk-core/Cargo.toml deleted file mode 100644 index a5186be5e..000000000 --- a/czech-file-knife/cfk-core/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "cfk-core" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Core traits and types for Czech File Knife" - -[dependencies] -async-trait.workspace = true -bytes.workspace = true -chrono.workspace = true -futures.workspace = true -serde.workspace = true -thiserror.workspace = true -tokio = { workspace = true, features = ["sync"] } diff --git a/czech-file-knife/cfk-core/src/backend.rs b/czech-file-knife/cfk-core/src/backend.rs deleted file mode 100644 index 93007d090..000000000 --- a/czech-file-knife/cfk-core/src/backend.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Storage backend trait - -use async_trait::async_trait; -use bytes::Bytes; -use std::pin::Pin; -use futures::Stream; - -use crate::{ - entry::{DirectoryListing, Entry}, - error::CfkResult, - operations::*, - VirtualPath, -}; - -/// Byte stream type -pub type ByteStream = Pin> + Send>>; - -/// Storage backend capabilities -#[derive(Debug, Clone, Default)] -pub struct StorageCapabilities { - pub read: bool, - pub write: bool, - pub delete: bool, - pub rename: bool, - pub copy: bool, - pub list: bool, - pub search: bool, - pub versioning: bool, - pub sharing: bool, - pub offline: bool, - pub streaming: bool, - pub resumable_uploads: bool, - pub content_hashing: bool, -} - -impl StorageCapabilities { - pub fn full() -> Self { - Self { - read: true, write: true, delete: true, rename: true, - copy: true, list: true, search: true, versioning: true, - sharing: true, offline: true, streaming: true, - resumable_uploads: true, content_hashing: true, - } - } - - pub fn read_only() -> Self { - Self { read: true, list: true, ..Default::default() } - } - - pub fn local_filesystem() -> Self { - Self { - read: true, write: true, delete: true, rename: true, - copy: true, list: true, search: true, offline: true, - streaming: true, content_hashing: true, - ..Default::default() - } - } -} - -/// Space information -#[derive(Debug, Clone, Default)] -pub struct SpaceInfo { - pub total: Option, - pub used: Option, - pub available: Option, -} - -impl SpaceInfo { - pub fn unknown() -> Self { - Self::default() - } -} - -/// File version information -#[derive(Debug, Clone)] -pub struct FileVersion { - pub id: String, - pub modified: chrono::DateTime, - pub size: Option, - pub author: Option, -} - -/// Search options -#[derive(Debug, Clone, Default)] -pub struct SearchOptions { - pub query: String, - pub path: Option, - pub recursive: bool, - pub limit: Option, -} - -/// Storage backend trait -#[async_trait] -pub trait StorageBackend: Send + Sync { - fn id(&self) -> &str; - fn display_name(&self) -> &str; - fn capabilities(&self) -> &StorageCapabilities; - - async fn is_available(&self) -> bool; - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult; - async fn list_directory(&self, path: &VirtualPath, options: &ListOptions) -> CfkResult; - async fn read_file(&self, path: &VirtualPath, options: &ReadOptions) -> CfkResult; - async fn write_file(&self, path: &VirtualPath, data: Bytes, options: &WriteOptions) -> CfkResult; - async fn write_file_stream(&self, path: &VirtualPath, stream: ByteStream, size_hint: Option, options: &WriteOptions) -> CfkResult; - async fn create_directory(&self, path: &VirtualPath) -> CfkResult; - async fn delete(&self, path: &VirtualPath, options: &DeleteOptions) -> CfkResult<()>; - async fn copy(&self, source: &VirtualPath, dest: &VirtualPath, options: &CopyOptions) -> CfkResult; - async fn rename(&self, source: &VirtualPath, dest: &VirtualPath, options: &MoveOptions) -> CfkResult; - async fn get_space_info(&self) -> CfkResult; - - // Optional methods with defaults - async fn search(&self, _options: &SearchOptions) -> CfkResult> { - Err(crate::CfkError::Unsupported("Search not supported".into())) - } - - async fn get_versions(&self, _path: &VirtualPath) -> CfkResult> { - Err(crate::CfkError::Unsupported("Versioning not supported".into())) - } - - async fn get_version(&self, _path: &VirtualPath, _version_id: &str) -> CfkResult { - Err(crate::CfkError::Unsupported("Versioning not supported".into())) - } -} diff --git a/czech-file-knife/cfk-core/src/entry.rs b/czech-file-knife/cfk-core/src/entry.rs deleted file mode 100644 index 7f6292659..000000000 --- a/czech-file-knife/cfk-core/src/entry.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! File system entries - -use crate::{Metadata, VirtualPath}; -use serde::{Deserialize, Serialize}; - -/// Entry kind -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntryKind { - File, - Directory, - Symlink, - Unknown, -} - -/// A file system entry -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Entry { - pub path: VirtualPath, - pub kind: EntryKind, - pub metadata: Metadata, -} - -impl Entry { - pub fn file(path: VirtualPath, metadata: Metadata) -> Self { - Self { path, kind: EntryKind::File, metadata } - } - - pub fn directory(path: VirtualPath, metadata: Metadata) -> Self { - Self { path, kind: EntryKind::Directory, metadata } - } - - pub fn is_file(&self) -> bool { - self.kind == EntryKind::File - } - - pub fn is_directory(&self) -> bool { - self.kind == EntryKind::Directory - } - - pub fn name(&self) -> Option<&str> { - self.path.name() - } - - pub fn size(&self) -> Option { - self.metadata.size - } -} - -/// Directory listing result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DirectoryListing { - pub path: VirtualPath, - pub entries: Vec, - pub cursor: Option, - pub has_more: bool, -} - -impl DirectoryListing { - pub fn new(path: VirtualPath, entries: Vec) -> Self { - Self { path, entries, cursor: None, has_more: false } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_path(p: &str) -> VirtualPath { - VirtualPath::new("local", p) - } - - #[test] - fn test_entry_file() { - let entry = Entry::file(make_path("/home/user/file.txt"), Metadata::new()); - assert!(entry.is_file()); - assert!(!entry.is_directory()); - assert_eq!(entry.kind, EntryKind::File); - } - - #[test] - fn test_entry_directory() { - let entry = Entry::directory(make_path("/home/user"), Metadata::new()); - assert!(entry.is_directory()); - assert!(!entry.is_file()); - assert_eq!(entry.kind, EntryKind::Directory); - } - - #[test] - fn test_entry_name() { - let entry = Entry::file(make_path("/home/user/document.pdf"), Metadata::new()); - assert_eq!(entry.name(), Some("document.pdf")); - } - - #[test] - fn test_entry_size() { - let mut meta = Metadata::new(); - meta.size = Some(1024); - let entry = Entry::file(make_path("/file.txt"), meta); - assert_eq!(entry.size(), Some(1024)); - } - - #[test] - fn test_directory_listing() { - let root = make_path("/home"); - let entries = vec![ - Entry::directory(make_path("/home/user1"), Metadata::new()), - Entry::directory(make_path("/home/user2"), Metadata::new()), - ]; - let listing = DirectoryListing::new(root.clone(), entries); - - assert_eq!(listing.path, root); - assert_eq!(listing.entries.len(), 2); - assert!(!listing.has_more); - assert!(listing.cursor.is_none()); - } -} diff --git a/czech-file-knife/cfk-core/src/error.rs b/czech-file-knife/cfk-core/src/error.rs deleted file mode 100644 index da1c3df7d..000000000 --- a/czech-file-knife/cfk-core/src/error.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Error types for Czech File Knife - -use thiserror::Error; - -/// Result type alias -pub type CfkResult = Result; - -/// Main error type -#[derive(Error, Debug)] -pub enum CfkError { - #[error("Path not found: {0}")] - NotFound(String), - - #[error("Already exists: {0}")] - AlreadyExists(String), - - #[error("Permission denied: {0}")] - PermissionDenied(String), - - #[error("Not a directory: {0}")] - NotADirectory(String), - - #[error("Not a file: {0}")] - NotAFile(String), - - #[error("Directory not empty: {0}")] - DirectoryNotEmpty(String), - - #[error("Invalid path: {0}")] - InvalidPath(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Network error: {0}")] - Network(String), - - #[error("Authentication required: {0}")] - AuthRequired(String), - - #[error("Authentication failed: {0}")] - AuthFailed(String), - - #[error("Token expired")] - TokenExpired, - - #[error("Rate limited: retry after {retry_after_secs:?}s")] - RateLimited { retry_after_secs: Option }, - - #[error("Provider API error ({provider}): {message}")] - ProviderApi { provider: String, message: String }, - - #[error("Quota exceeded: {0}")] - QuotaExceeded(String), - - #[error("Conflict: {0}")] - Conflict(String), - - #[error("Unsupported operation: {0}")] - Unsupported(String), - - #[error("Serialization error: {0}")] - Serialization(String), - - #[error("Cache error: {0}")] - Cache(String), - - #[error("Backend not found: {0}")] - BackendNotFound(String), - - #[error("Offline and no cached version")] - OfflineNoCache, - - #[error("Checksum mismatch")] - ChecksumMismatch, - - #[error("Timeout")] - Timeout, - - #[error("Cancelled")] - Cancelled, - - #[error("{0}")] - Other(String), -} - -impl CfkError { - pub fn is_retryable(&self) -> bool { - matches!( - self, - CfkError::Network(_) - | CfkError::RateLimited { .. } - | CfkError::Timeout - | CfkError::TokenExpired - ) - } - - pub fn is_auth_error(&self) -> bool { - matches!( - self, - CfkError::AuthRequired(_) | CfkError::AuthFailed(_) | CfkError::TokenExpired - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_retryable() { - assert!(CfkError::Network("connection reset".into()).is_retryable()); - assert!(CfkError::RateLimited { retry_after_secs: Some(30) }.is_retryable()); - assert!(CfkError::Timeout.is_retryable()); - assert!(CfkError::TokenExpired.is_retryable()); - - assert!(!CfkError::NotFound("file.txt".into()).is_retryable()); - assert!(!CfkError::PermissionDenied("/root".into()).is_retryable()); - assert!(!CfkError::Cancelled.is_retryable()); - } - - #[test] - fn test_is_auth_error() { - assert!(CfkError::AuthRequired("login needed".into()).is_auth_error()); - assert!(CfkError::AuthFailed("bad password".into()).is_auth_error()); - assert!(CfkError::TokenExpired.is_auth_error()); - - assert!(!CfkError::Network("timeout".into()).is_auth_error()); - assert!(!CfkError::NotFound("file.txt".into()).is_auth_error()); - } - - #[test] - fn test_error_display() { - let err = CfkError::NotFound("/path/to/file".into()); - assert_eq!(format!("{}", err), "Path not found: /path/to/file"); - - let err = CfkError::RateLimited { retry_after_secs: Some(60) }; - assert!(format!("{}", err).contains("60")); - } - - #[test] - fn test_from_io_error() { - let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); - let cfk_err: CfkError = io_err.into(); - assert!(matches!(cfk_err, CfkError::Io(_))); - } -} diff --git a/czech-file-knife/cfk-core/src/lib.rs b/czech-file-knife/cfk-core/src/lib.rs deleted file mode 100644 index 3c45ab9e8..000000000 --- a/czech-file-knife/cfk-core/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Czech File Knife Core -//! -//! Core traits, types, and abstractions for the unified filesystem interface. - -pub mod backend; -pub mod entry; -pub mod error; -pub mod metadata; -pub mod operations; -pub mod path; -pub mod platform; - -pub use backend::{StorageBackend, StorageCapabilities}; -pub use entry::{Entry, EntryKind}; -pub use error::{CfkError, CfkResult}; -pub use metadata::Metadata; -pub use path::VirtualPath; diff --git a/czech-file-knife/cfk-core/src/metadata.rs b/czech-file-knife/cfk-core/src/metadata.rs deleted file mode 100644 index b260fb7b2..000000000 --- a/czech-file-knife/cfk-core/src/metadata.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! File and directory metadata - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// File/directory metadata -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Metadata { - pub size: Option, - pub created: Option>, - pub modified: Option>, - pub accessed: Option>, - pub permissions: Option, - pub content_hash: Option, - pub mime_type: Option, - pub provider_id: Option, - pub revision: Option, - pub custom: HashMap, -} - -/// Unix-style permissions -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Permissions { - pub mode: u32, -} - -impl Permissions { - pub fn new(mode: u32) -> Self { - Self { mode } - } - - pub fn is_readable(&self) -> bool { - self.mode & 0o444 != 0 - } - - pub fn is_writable(&self) -> bool { - self.mode & 0o222 != 0 - } - - pub fn is_executable(&self) -> bool { - self.mode & 0o111 != 0 - } -} - -impl Metadata { - pub fn new() -> Self { - Self::default() - } - - pub fn with_size(mut self, size: u64) -> Self { - self.size = Some(size); - self - } - - pub fn with_modified(mut self, modified: DateTime) -> Self { - self.modified = Some(modified); - self - } -} diff --git a/czech-file-knife/cfk-core/src/operations.rs b/czech-file-knife/cfk-core/src/operations.rs deleted file mode 100644 index 2682c00d0..000000000 --- a/czech-file-knife/cfk-core/src/operations.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Operation options - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ListOptions { - pub recursive: bool, - pub include_hidden: bool, - pub limit: Option, - pub cursor: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ReadOptions { - pub range: Option<(u64, u64)>, - pub use_cache: bool, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct WriteOptions { - pub overwrite: bool, - pub create_parents: bool, - pub content_hash: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct CopyOptions { - pub overwrite: bool, - pub preserve_metadata: bool, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct MoveOptions { - pub overwrite: bool, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct DeleteOptions { - pub recursive: bool, - pub force: bool, -} diff --git a/czech-file-knife/cfk-core/src/path.rs b/czech-file-knife/cfk-core/src/path.rs deleted file mode 100644 index 9b680fea4..000000000 --- a/czech-file-knife/cfk-core/src/path.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Virtual path abstraction - -use serde::{Deserialize, Serialize}; -use std::fmt; - -/// Virtual path representing a location across any backend -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VirtualPath { - /// Backend identifier (e.g., "local", "dropbox", "gdrive") - pub backend: String, - /// Path segments - pub segments: Vec, -} - -impl VirtualPath { - pub fn new(backend: impl Into, path: impl AsRef) -> Self { - let path = path.as_ref(); - let segments = path - .split('/') - .filter(|s| !s.is_empty()) - .map(String::from) - .collect(); - Self { - backend: backend.into(), - segments, - } - } - - pub fn root(backend: impl Into) -> Self { - Self { - backend: backend.into(), - segments: Vec::new(), - } - } - - pub fn join(&self, name: impl AsRef) -> Self { - let mut segments = self.segments.clone(); - for part in name.as_ref().split('/').filter(|s| !s.is_empty()) { - if part == ".." { - segments.pop(); - } else if part != "." { - segments.push(part.to_string()); - } - } - Self { - backend: self.backend.clone(), - segments, - } - } - - pub fn parent(&self) -> Option { - if self.segments.is_empty() { - None - } else { - let mut segments = self.segments.clone(); - segments.pop(); - Some(Self { - backend: self.backend.clone(), - segments, - }) - } - } - - pub fn name(&self) -> Option<&str> { - self.segments.last().map(|s| s.as_str()) - } - - pub fn extension(&self) -> Option<&str> { - self.name().and_then(|n| n.rsplit_once('.')).map(|(_, ext)| ext) - } - - pub fn is_root(&self) -> bool { - self.segments.is_empty() - } - - pub fn to_path_string(&self) -> String { - if self.segments.is_empty() { - "/".to_string() - } else { - format!("/{}", self.segments.join("/")) - } - } - - pub fn to_uri(&self) -> String { - format!("cfk://{}{}", self.backend, self.to_path_string()) - } - - pub fn parse_uri(uri: &str) -> Option { - let uri = uri.strip_prefix("cfk://")?; - let (backend, path) = uri.split_once('/').unwrap_or((uri, "")); - Some(Self::new(backend, path)) - } -} - -impl fmt::Display for VirtualPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_uri()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new() { - let path = VirtualPath::new("local", "/home/user/docs"); - assert_eq!(path.backend, "local"); - assert_eq!(path.segments, vec!["home", "user", "docs"]); - } - - #[test] - fn test_new_handles_empty_segments() { - let path = VirtualPath::new("local", "//home//user//"); - assert_eq!(path.segments, vec!["home", "user"]); - } - - #[test] - fn test_root() { - let path = VirtualPath::root("dropbox"); - assert_eq!(path.backend, "dropbox"); - assert!(path.segments.is_empty()); - assert!(path.is_root()); - } - - #[test] - fn test_join() { - let root = VirtualPath::root("local"); - let path = root.join("home").join("user"); - assert_eq!(path.segments, vec!["home", "user"]); - } - - #[test] - fn test_join_with_dotdot() { - let path = VirtualPath::new("local", "/home/user/docs"); - let new_path = path.join("../pictures"); - assert_eq!(new_path.segments, vec!["home", "user", "pictures"]); - } - - #[test] - fn test_join_with_dot() { - let path = VirtualPath::new("local", "/home/user"); - let new_path = path.join("./docs"); - assert_eq!(new_path.segments, vec!["home", "user", "docs"]); - } - - #[test] - fn test_parent() { - let path = VirtualPath::new("local", "/home/user/docs"); - let parent = path.parent().unwrap(); - assert_eq!(parent.segments, vec!["home", "user"]); - } - - #[test] - fn test_parent_of_root() { - let root = VirtualPath::root("local"); - assert!(root.parent().is_none()); - } - - #[test] - fn test_name() { - let path = VirtualPath::new("local", "/home/user/file.txt"); - assert_eq!(path.name(), Some("file.txt")); - } - - #[test] - fn test_name_of_root() { - let root = VirtualPath::root("local"); - assert!(root.name().is_none()); - } - - #[test] - fn test_extension() { - let path = VirtualPath::new("local", "/home/user/file.txt"); - assert_eq!(path.extension(), Some("txt")); - - let path_no_ext = VirtualPath::new("local", "/home/user/file"); - assert!(path_no_ext.extension().is_none()); - - let path_multi = VirtualPath::new("local", "/archive.tar.gz"); - assert_eq!(path_multi.extension(), Some("gz")); - } - - #[test] - fn test_to_path_string() { - let root = VirtualPath::root("local"); - assert_eq!(root.to_path_string(), "/"); - - let path = VirtualPath::new("local", "/home/user"); - assert_eq!(path.to_path_string(), "/home/user"); - } - - #[test] - fn test_to_uri() { - let path = VirtualPath::new("dropbox", "/Documents/file.txt"); - assert_eq!(path.to_uri(), "cfk://dropbox/Documents/file.txt"); - - let root = VirtualPath::root("gdrive"); - assert_eq!(root.to_uri(), "cfk://gdrive/"); - } - - #[test] - fn test_parse_uri() { - let path = VirtualPath::parse_uri("cfk://local/home/user").unwrap(); - assert_eq!(path.backend, "local"); - assert_eq!(path.segments, vec!["home", "user"]); - } - - #[test] - fn test_parse_uri_root() { - let path = VirtualPath::parse_uri("cfk://dropbox").unwrap(); - assert_eq!(path.backend, "dropbox"); - assert!(path.segments.is_empty()); - } - - #[test] - fn test_parse_uri_invalid() { - assert!(VirtualPath::parse_uri("http://example.com").is_none()); - assert!(VirtualPath::parse_uri("/local/path").is_none()); - } - - #[test] - fn test_display() { - let path = VirtualPath::new("s3", "/bucket/key"); - assert_eq!(format!("{}", path), "cfk://s3/bucket/key"); - } - - #[test] - fn test_equality() { - let path1 = VirtualPath::new("local", "/home/user"); - let path2 = VirtualPath::new("local", "home/user"); - assert_eq!(path1, path2); - } -} diff --git a/czech-file-knife/cfk-core/src/platform.rs b/czech-file-knife/cfk-core/src/platform.rs deleted file mode 100644 index e47c587f1..000000000 --- a/czech-file-knife/cfk-core/src/platform.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Platform-specific abstractions -//! -//! Supports: Linux, macOS, Windows, iOS, Android, Minix, z/OS, RISC-V - - -/// Platform capabilities -#[derive(Debug, Clone)] -pub struct PlatformCapabilities { - pub fuse_available: bool, - pub async_io: bool, - pub file_watching: bool, - pub symlinks: bool, - pub hard_links: bool, - pub extended_attributes: bool, - pub sparse_files: bool, - pub memory_mapping: bool, -} - -impl PlatformCapabilities { - pub fn detect() -> Self { - #[cfg(target_os = "linux")] - return Self::linux(); - - #[cfg(target_os = "macos")] - return Self::macos(); - - #[cfg(target_os = "windows")] - return Self::windows(); - - #[cfg(target_os = "ios")] - return Self::ios(); - - #[cfg(target_os = "android")] - return Self::android(); - - #[cfg(not(any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "ios", - target_os = "android" - )))] - return Self::minimal(); - } - - pub fn linux() -> Self { - Self { - fuse_available: true, - async_io: true, - file_watching: true, - symlinks: true, - hard_links: true, - extended_attributes: true, - sparse_files: true, - memory_mapping: true, - } - } - - pub fn macos() -> Self { - Self { - fuse_available: true, // via macFUSE - async_io: true, - file_watching: true, - symlinks: true, - hard_links: true, - extended_attributes: true, - sparse_files: true, - memory_mapping: true, - } - } - - pub fn windows() -> Self { - Self { - fuse_available: true, // via WinFsp - async_io: true, - file_watching: true, - symlinks: true, // requires admin or dev mode - hard_links: true, - extended_attributes: false, // different model (ADS) - sparse_files: true, - memory_mapping: true, - } - } - - pub fn ios() -> Self { - Self { - fuse_available: false, - async_io: true, - file_watching: false, - symlinks: false, - hard_links: false, - extended_attributes: false, - sparse_files: false, - memory_mapping: true, - } - } - - pub fn android() -> Self { - Self { - fuse_available: false, // root only - async_io: true, - file_watching: true, - symlinks: true, - hard_links: false, - extended_attributes: false, - sparse_files: false, - memory_mapping: true, - } - } - - pub fn minimal() -> Self { - Self { - fuse_available: false, - async_io: false, - file_watching: false, - symlinks: false, - hard_links: false, - extended_attributes: false, - sparse_files: false, - memory_mapping: false, - } - } -} - -/// z/OS dataset path conversion -pub mod zos { - /// Convert VirtualPath to z/OS dataset name - /// cfk://zos/SYS1/PARMLIB/IEASYS00 → SYS1.PARMLIB(IEASYS00) - pub fn to_dataset_name(segments: &[String]) -> String { - if segments.is_empty() { - return String::new(); - } - - let mut parts = segments.to_vec(); - if let Some(member) = parts.pop() { - if parts.is_empty() { - member - } else { - format!("{}({})", parts.join("."), member) - } - } else { - String::new() - } - } - - /// Convert z/OS dataset name to path segments - pub fn from_dataset_name(dsn: &str) -> Vec { - if let Some((prefix, member)) = dsn.rsplit_once('(') { - let member = member.trim_end_matches(')'); - let mut segments: Vec = prefix.split('.').map(String::from).collect(); - segments.push(member.to_string()); - segments - } else { - dsn.split('.').map(String::from).collect() - } - } -} - -/// EBCDIC/ASCII transcoding for z/OS -pub mod encoding { - /// Simple EBCDIC to ASCII (US EBCDIC code page 037) - pub fn ebcdic_to_ascii(input: &[u8]) -> Vec { - input.iter().map(|&b| EBCDIC_TO_ASCII[b as usize]).collect() - } - - /// Simple ASCII to EBCDIC - pub fn ascii_to_ebcdic(input: &[u8]) -> Vec { - input.iter().map(|&b| ASCII_TO_EBCDIC[b as usize]).collect() - } - - // EBCDIC code page 037 to ASCII mapping (simplified) - static EBCDIC_TO_ASCII: [u8; 256] = [ - 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, // 0x00-0x07 - 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // 0x08-0x0F - 0x10, 0x11, 0x12, 0x13, 0x9D, 0x85, 0x08, 0x87, // 0x10-0x17 - 0x18, 0x19, 0x92, 0x8F, 0x1C, 0x1D, 0x1E, 0x1F, // 0x18-0x1F - 0x80, 0x81, 0x82, 0x83, 0x84, 0x0A, 0x17, 0x1B, // 0x20-0x27 - 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x05, 0x06, 0x07, // 0x28-0x2F - 0x90, 0x91, 0x16, 0x93, 0x94, 0x95, 0x96, 0x04, // 0x30-0x37 - 0x98, 0x99, 0x9A, 0x9B, 0x14, 0x15, 0x9E, 0x1A, // 0x38-0x3F - 0x20, 0xA0, 0xE2, 0xE4, 0xE0, 0xE1, 0xE3, 0xE5, // 0x40-0x47 - 0xE7, 0xF1, 0xA2, 0x2E, 0x3C, 0x28, 0x2B, 0x7C, // 0x48-0x4F - 0x26, 0xE9, 0xEA, 0xEB, 0xE8, 0xED, 0xEE, 0xEF, // 0x50-0x57 - 0xEC, 0xDF, 0x21, 0x24, 0x2A, 0x29, 0x3B, 0xAC, // 0x58-0x5F - 0x2D, 0x2F, 0xC2, 0xC4, 0xC0, 0xC1, 0xC3, 0xC5, // 0x60-0x67 - 0xC7, 0xD1, 0xA6, 0x2C, 0x25, 0x5F, 0x3E, 0x3F, // 0x68-0x6F - 0xF8, 0xC9, 0xCA, 0xCB, 0xC8, 0xCD, 0xCE, 0xCF, // 0x70-0x77 - 0xCC, 0x60, 0x3A, 0x23, 0x40, 0x27, 0x3D, 0x22, // 0x78-0x7F - // ... continuing would fill all 256 bytes - 0xD8, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0xAB, 0xBB, 0xF0, 0xFD, 0xFE, 0xB1, - 0xB0, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, - 0x71, 0x72, 0xAA, 0xBA, 0xE6, 0xB8, 0xC6, 0xA4, - 0xB5, 0x7E, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7A, 0xA1, 0xBF, 0xD0, 0xDD, 0xDE, 0xAE, - 0x5E, 0xA3, 0xA5, 0xB7, 0xA9, 0xA7, 0xB6, 0xBC, - 0xBD, 0xBE, 0x5B, 0x5D, 0xAF, 0xA8, 0xB4, 0xD7, - 0x7B, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x48, 0x49, 0xAD, 0xF4, 0xF6, 0xF2, 0xF3, 0xF5, - 0x7D, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, - 0x51, 0x52, 0xB9, 0xFB, 0xFC, 0xF9, 0xFA, 0xFF, - 0x5C, 0xF7, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, - 0x59, 0x5A, 0xB2, 0xD4, 0xD6, 0xD2, 0xD3, 0xD5, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, - 0x38, 0x39, 0xB3, 0xDB, 0xDC, 0xD9, 0xDA, 0x9F, - ]; - - // ASCII to EBCDIC (inverse mapping) - static ASCII_TO_EBCDIC: [u8; 256] = { - let mut table = [0x3Fu8; 256]; // Default to '?' - let mut i = 0; - while i < 256 { - let ascii_val = EBCDIC_TO_ASCII[i]; - table[ascii_val as usize] = i as u8; - i += 1; - } - table - }; -} diff --git a/czech-file-knife/cfk-integrations/Cargo.toml b/czech-file-knife/cfk-integrations/Cargo.toml deleted file mode 100644 index 893f2fa51..000000000 --- a/czech-file-knife/cfk-integrations/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "cfk-integrations" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "External tool integrations: aria2, agrep, pandoc, OCR, eza" - -[features] -default = [] -aria2 = [] -agrep = [] -pandoc = [] -ocr = ["tesseract"] -tesseract = [] -eza = [] -all = ["aria2", "agrep", "pandoc", "ocr", "eza"] - -[dependencies] -cfk-core = { path = "../cfk-core" } -async-trait.workspace = true -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tokio = { workspace = true, features = ["process"] } -tracing.workspace = true -regex.workspace = true diff --git a/czech-file-knife/cfk-integrations/src/agrep.rs b/czech-file-knife/cfk-integrations/src/agrep.rs deleted file mode 100644 index afb8aef82..000000000 --- a/czech-file-knife/cfk-integrations/src/agrep.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! agrep integration for approximate/fuzzy grep -//! -//! agrep allows errors in pattern matching (Levenshtein distance) - -use crate::{run_command, CfkResult}; -use serde::{Deserialize, Serialize}; -use std::path::Path; - -/// agrep match result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgrepMatch { - pub file: String, - pub line_number: usize, - pub line: String, - pub errors: u8, -} - -/// agrep search options -#[derive(Debug, Clone, Default)] -pub struct AgrepOptions { - pub max_errors: u8, // -k N: allow N errors - pub case_insensitive: bool, // -i - pub word_match: bool, // -w - pub line_match: bool, // -x (whole line) - pub count_only: bool, // -c - pub files_only: bool, // -l - pub recursive: bool, // -r -} - -/// Search for approximate pattern matches -pub async fn search( - pattern: &str, - path: &Path, - options: &AgrepOptions, -) -> CfkResult> { - let mut args = vec!["-n".to_string()]; // line numbers - - if options.max_errors > 0 { - args.push(format!("-{}", options.max_errors)); - } - if options.case_insensitive { - args.push("-i".to_string()); - } - if options.word_match { - args.push("-w".to_string()); - } - if options.line_match { - args.push("-x".to_string()); - } - if options.recursive { - args.push("-r".to_string()); - } - - args.push(pattern.to_string()); - args.push(path.to_string_lossy().to_string()); - - let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); - let output = run_command("agrep", &args_ref).await?; - - let mut matches = Vec::new(); - for line in String::from_utf8_lossy(&output.stdout).lines() { - if let Some((file_line, content)) = line.split_once(':') { - if let Some((file, line_num)) = file_line.rsplit_once(':') { - matches.push(AgrepMatch { - file: file.to_string(), - line_number: line_num.parse().unwrap_or(0), - line: content.to_string(), - errors: options.max_errors, - }); - } - } - } - - Ok(matches) -} - -/// Fuzzy file name search -pub async fn find_files( - pattern: &str, - dir: &Path, - max_errors: u8, -) -> CfkResult> { - // Use find + agrep for fuzzy filename matching - let find_output = run_command("find", &[ - dir.to_str().unwrap(), - "-type", "f", - "-print" - ]).await?; - - let mut args = vec![format!("-{}", max_errors), pattern.to_string()]; - let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); - - // Pipe find output to agrep (simplified - actual impl would use pipes) - let files = String::from_utf8_lossy(&find_output.stdout) - .lines() - .filter(|f| f.contains(pattern) || pattern.len() < 3) // Simplified - .map(String::from) - .collect(); - - Ok(files) -} diff --git a/czech-file-knife/cfk-integrations/src/aria2.rs b/czech-file-knife/cfk-integrations/src/aria2.rs deleted file mode 100644 index d836ce628..000000000 --- a/czech-file-knife/cfk-integrations/src/aria2.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! aria2 integration for high-speed downloads -//! -//! aria2 supports: HTTP/HTTPS, FTP, SFTP, BitTorrent, Metalink - -use crate::{run_command, CfkResult}; -use serde::{Deserialize, Serialize}; -use std::path::Path; - -/// aria2 download options -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Aria2Options { - pub connections: u8, // max connections per server (default: 5) - pub split: u8, // split file into N parts (default: 5) - pub min_split_size: String, // minimum split size (default: "20M") - pub continue_download: bool, - pub max_speed: Option, // e.g., "1M" - pub user_agent: Option, - pub headers: Vec<(String, String)>, -} - -/// Download a file using aria2 -pub async fn download( - url: &str, - output: &Path, - options: &Aria2Options, -) -> CfkResult<()> { - let mut args = vec![ - url, - "-d", output.parent().unwrap_or(Path::new(".")).to_str().unwrap(), - "-o", output.file_name().unwrap().to_str().unwrap(), - "-x", &options.connections.to_string(), - "-s", &options.split.to_string(), - "-k", &options.min_split_size, - ]; - - if options.continue_download { - args.push("-c"); - } - - let output = run_command("aria2c", &args).await?; - if !output.status.success() { - return Err(cfk_core::CfkError::Other( - String::from_utf8_lossy(&output.stderr).to_string() - )); - } - Ok(()) -} - -/// Download multiple files in parallel -pub async fn download_batch(urls: &[&str], output_dir: &Path) -> CfkResult<()> { - let mut args = vec![ - "-d", output_dir.to_str().unwrap(), - "-x", "5", - "-s", "5", - "-j", "5", // concurrent downloads - ]; - args.extend(urls.iter().map(|s| *s)); - - let output = run_command("aria2c", &args).await?; - if !output.status.success() { - return Err(cfk_core::CfkError::Other( - String::from_utf8_lossy(&output.stderr).to_string() - )); - } - Ok(()) -} - -/// Check aria2 version -pub async fn version() -> CfkResult { - let output = run_command("aria2c", &["--version"]).await?; - Ok(String::from_utf8_lossy(&output.stdout) - .lines() - .next() - .unwrap_or("unknown") - .to_string()) -} diff --git a/czech-file-knife/cfk-integrations/src/lib.rs b/czech-file-knife/cfk-integrations/src/lib.rs deleted file mode 100644 index 9ac2a09c0..000000000 --- a/czech-file-knife/cfk-integrations/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! External tool integrations for Czech File Knife -//! -//! Integrates: aria2, agrep, pandoc, tesseract OCR, eza - -#[cfg(feature = "aria2")] -pub mod aria2; - -#[cfg(feature = "agrep")] -pub mod agrep; - -#[cfg(feature = "pandoc")] -pub mod pandoc; - -#[cfg(feature = "ocr")] -pub mod ocr; - -#[cfg(feature = "eza")] -pub mod eza; - -use cfk_core::error::{CfkError, CfkResult}; -use std::process::Output; -use tokio::process::Command; - -/// Check if an external tool is available -pub async fn check_tool(name: &str) -> bool { - Command::new("which") - .arg(name) - .output() - .await - .map(|o| o.status.success()) - .unwrap_or(false) -} - -/// Run an external command and get output -pub async fn run_command(program: &str, args: &[&str]) -> CfkResult { - Command::new(program) - .args(args) - .output() - .await - .map_err(|e| CfkError::Other(format!("Failed to run {}: {}", program, e))) -} - -/// Tool availability status -#[derive(Debug, Clone)] -pub struct ToolStatus { - pub aria2: bool, - pub agrep: bool, - pub pandoc: bool, - pub tesseract: bool, - pub eza: bool, -} - -impl ToolStatus { - pub async fn detect() -> Self { - Self { - aria2: check_tool("aria2c").await, - agrep: check_tool("agrep").await, - pandoc: check_tool("pandoc").await, - tesseract: check_tool("tesseract").await, - eza: check_tool("eza").await || check_tool("exa").await, - } - } -} diff --git a/czech-file-knife/cfk-integrations/src/pandoc.rs b/czech-file-knife/cfk-integrations/src/pandoc.rs deleted file mode 100644 index fd840a007..000000000 --- a/czech-file-knife/cfk-integrations/src/pandoc.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! pandoc integration for document format conversion -//! -//! Supports: markdown, docx, pdf, html, epub, rst, latex, and 40+ formats - -use crate::{run_command, CfkResult}; -use std::path::Path; - -/// Supported input/output formats -#[derive(Debug, Clone, Copy)] -pub enum Format { - Markdown, - Html, - Docx, - Pdf, - Epub, - Rst, - Latex, - Org, - Asciidoc, - Json, - Plain, -} - -impl Format { - pub fn as_str(&self) -> &'static str { - match self { - Format::Markdown => "markdown", - Format::Html => "html", - Format::Docx => "docx", - Format::Pdf => "pdf", - Format::Epub => "epub", - Format::Rst => "rst", - Format::Latex => "latex", - Format::Org => "org", - Format::Asciidoc => "asciidoc", - Format::Json => "json", - Format::Plain => "plain", - } - } -} - -/// Convert a file between formats -pub async fn convert( - input: &Path, - output: &Path, - from: Option, - to: Option, -) -> CfkResult<()> { - let mut args = vec![ - input.to_str().unwrap(), - "-o", output.to_str().unwrap(), - ]; - - let from_str; - let to_str; - - if let Some(f) = from { - from_str = f.as_str().to_string(); - args.extend(["-f", &from_str]); - } - if let Some(t) = to { - to_str = t.as_str().to_string(); - args.extend(["-t", &to_str]); - } - - let output = run_command("pandoc", &args).await?; - if !output.status.success() { - return Err(cfk_core::CfkError::Other( - String::from_utf8_lossy(&output.stderr).to_string() - )); - } - Ok(()) -} - -/// Convert string content between formats -pub async fn convert_string( - content: &str, - from: Format, - to: Format, -) -> CfkResult { - use tokio::process::Command; - use tokio::io::AsyncWriteExt; - - let mut child = Command::new("pandoc") - .args(["-f", from.as_str(), "-t", to.as_str()]) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn() - .map_err(|e| cfk_core::CfkError::Other(e.to_string()))?; - - if let Some(mut stdin) = child.stdin.take() { - stdin.write_all(content.as_bytes()).await - .map_err(|e| cfk_core::CfkError::Other(e.to_string()))?; - } - - let output = child.wait_with_output().await - .map_err(|e| cfk_core::CfkError::Other(e.to_string()))?; - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -/// Get pandoc version -pub async fn version() -> CfkResult { - let output = run_command("pandoc", &["--version"]).await?; - Ok(String::from_utf8_lossy(&output.stdout) - .lines() - .next() - .unwrap_or("unknown") - .to_string()) -} diff --git a/czech-file-knife/cfk-ios/Cargo.toml b/czech-file-knife/cfk-ios/Cargo.toml deleted file mode 100644 index 05c5e483f..000000000 --- a/czech-file-knife/cfk-ios/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "cfk-ios" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -authors.workspace = true -description = "iOS File Provider extension for Czech File Knife" - -[lib] -crate-type = ["staticlib", "cdylib"] - -[dependencies] -cfk-core = { path = "../cfk-core" } -cfk-providers = { path = "../cfk-providers" } -cfk-cache = { path = "../cfk-cache" } - -# FFI -libc = "0.2" -once_cell = "1.19" - -# Async -tokio = { workspace = true } -async-trait.workspace = true -futures.workspace = true - -# Serialization -serde.workspace = true -serde_json.workspace = true - -# Error handling -thiserror.workspace = true - -# Logging -tracing.workspace = true -tracing-subscriber = { workspace = true } - -# Time -chrono.workspace = true - -# Bytes -bytes.workspace = true - -[target.'cfg(target_os = "ios")'.dependencies] -objc = "0.2" -objc-foundation = "0.1" -block = "0.1" - -[features] -default = [] -simulator = [] # iOS Simulator builds diff --git a/czech-file-knife/cfk-ios/src/domain.rs b/czech-file-knife/cfk-ios/src/domain.rs deleted file mode 100644 index d79d1328b..000000000 --- a/czech-file-knife/cfk-ios/src/domain.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! File Provider Domain management -//! -//! Maps to NSFileProviderDomain in iOS. - -use crate::error::{IosError, IosResult}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Unique domain identifier -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct DomainIdentifier(pub String); - -impl DomainIdentifier { - pub fn new(id: impl Into) -> Self { - Self(id.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -/// File Provider Domain -/// -/// Represents a storage location (e.g., a Dropbox account, Google Drive). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileDomain { - /// Unique identifier - pub identifier: DomainIdentifier, - /// Display name shown in Files app - pub display_name: String, - /// Backend type (dropbox, gdrive, onedrive, etc.) - pub backend_type: String, - /// Backend configuration as JSON - pub config_json: String, - /// Whether the domain is currently enabled - pub enabled: bool, - /// Path prefix for this domain - pub path_prefix: Option, - /// Custom metadata - #[serde(default)] - pub metadata: HashMap, -} - -impl FileDomain { - /// Create a new domain - pub fn new( - identifier: impl Into, - display_name: impl Into, - backend_type: impl Into, - ) -> Self { - Self { - identifier: DomainIdentifier::new(identifier), - display_name: display_name.into(), - backend_type: backend_type.into(), - config_json: "{}".to_string(), - enabled: true, - path_prefix: None, - metadata: HashMap::new(), - } - } - - /// Set configuration JSON - pub fn with_config(mut self, config: impl Into) -> Self { - self.config_json = config.into(); - self - } - - /// Set path prefix - pub fn with_prefix(mut self, prefix: impl Into) -> Self { - self.path_prefix = Some(prefix.into()); - self - } - - /// Check if this is a cloud backend - pub fn is_cloud(&self) -> bool { - matches!( - self.backend_type.as_str(), - "dropbox" | "gdrive" | "onedrive" | "box" | "icloud" | "s3" | "webdav" - ) - } -} - -/// Domain manager -pub struct DomainManager { - domains: Arc>>, - storage_path: std::path::PathBuf, -} - -impl DomainManager { - /// Create a new domain manager - pub fn new(storage_path: impl Into) -> Self { - Self { - domains: Arc::new(RwLock::new(HashMap::new())), - storage_path: storage_path.into(), - } - } - - /// Load domains from persistent storage - pub async fn load(&self) -> IosResult<()> { - let path = self.storage_path.join("domains.json"); - if path.exists() { - let data = tokio::fs::read_to_string(&path) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - let domains: Vec = serde_json::from_str(&data) - .map_err(|e| IosError::Ffi(format!("Failed to parse domains: {}", e)))?; - - let mut map = self.domains.write().await; - for domain in domains { - map.insert(domain.identifier.clone(), domain); - } - } - Ok(()) - } - - /// Save domains to persistent storage - pub async fn save(&self) -> IosResult<()> { - let domains: Vec = self.domains.read().await.values().cloned().collect(); - let data = serde_json::to_string_pretty(&domains) - .map_err(|e| IosError::Ffi(format!("Failed to serialize domains: {}", e)))?; - - tokio::fs::create_dir_all(&self.storage_path) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - let path = self.storage_path.join("domains.json"); - tokio::fs::write(&path, data) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - Ok(()) - } - - /// Add a domain - pub async fn add(&self, domain: FileDomain) -> IosResult<()> { - let mut domains = self.domains.write().await; - domains.insert(domain.identifier.clone(), domain); - drop(domains); - self.save().await - } - - /// Remove a domain - pub async fn remove(&self, id: &DomainIdentifier) -> IosResult> { - let mut domains = self.domains.write().await; - let removed = domains.remove(id); - drop(domains); - self.save().await?; - Ok(removed) - } - - /// Get a domain by ID - pub async fn get(&self, id: &DomainIdentifier) -> Option { - self.domains.read().await.get(id).cloned() - } - - /// List all domains - pub async fn list(&self) -> Vec { - self.domains.read().await.values().cloned().collect() - } - - /// Get enabled domains only - pub async fn list_enabled(&self) -> Vec { - self.domains - .read() - .await - .values() - .filter(|d| d.enabled) - .cloned() - .collect() - } - - /// Enable/disable a domain - pub async fn set_enabled(&self, id: &DomainIdentifier, enabled: bool) -> IosResult<()> { - let mut domains = self.domains.write().await; - if let Some(domain) = domains.get_mut(id) { - domain.enabled = enabled; - } - drop(domains); - self.save().await - } -} - -/// Domain change notification type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(i32)] -pub enum DomainChangeType { - Added = 0, - Removed = 1, - Updated = 2, -} diff --git a/czech-file-knife/cfk-ios/src/error.rs b/czech-file-knife/cfk-ios/src/error.rs deleted file mode 100644 index ee973b7af..000000000 --- a/czech-file-knife/cfk-ios/src/error.rs +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! iOS-specific error types - -use cfk_core::CfkError as CoreError; -use std::ffi::CString; -use thiserror::Error; - -/// iOS-specific errors -#[derive(Debug, Error)] -pub enum IosError { - #[error("Core error: {0}")] - Core(#[from] CoreError), - - #[error("Invalid identifier: {0}")] - InvalidIdentifier(String), - - #[error("Item not found: {0}")] - NotFound(String), - - #[error("Operation not supported: {0}")] - NotSupported(String), - - #[error("Authentication required")] - AuthRequired, - - #[error("Network unavailable")] - NetworkUnavailable, - - #[error("Quota exceeded")] - QuotaExceeded, - - #[error("Conflict: {0}")] - Conflict(String), - - #[error("Server error: {0}")] - ServerError(String), - - #[error("FFI error: {0}")] - Ffi(String), - - #[error("Sync error: {0}")] - Sync(String), -} - -/// iOS result type -pub type IosResult = Result; - -/// NSFileProviderError codes (matching Apple's NSFileProviderErrorCode) -#[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileProviderErrorCode { - /// No error - Success = 0, - /// Item not found - NoSuchItem = -1000, - /// Item already exists - ItemAlreadyExists = -1001, - /// Not authenticated - NotAuthenticated = -1002, - /// Server unreachable - ServerUnreachable = -1003, - /// Quota exceeded - QuotaExceeded = -1004, - /// Invalid filename - FilenameInvalid = -1005, - /// Version out of date - VersionOutOfDate = -1006, - /// Page expired - PageExpired = -1007, - /// Sync anchor expired - SyncAnchorExpired = -1008, - /// Insufficient quota - InsufficientQuota = -1009, - /// Cannot sync - CannotSync = -1010, - /// Unknown error - Unknown = -9999, -} - -impl From<&IosError> for FileProviderErrorCode { - fn from(err: &IosError) -> Self { - match err { - IosError::NotFound(_) => FileProviderErrorCode::NoSuchItem, - IosError::AuthRequired => FileProviderErrorCode::NotAuthenticated, - IosError::NetworkUnavailable => FileProviderErrorCode::ServerUnreachable, - IosError::QuotaExceeded => FileProviderErrorCode::QuotaExceeded, - IosError::Conflict(_) => FileProviderErrorCode::VersionOutOfDate, - IosError::Sync(_) => FileProviderErrorCode::CannotSync, - _ => FileProviderErrorCode::Unknown, - } - } -} - -/// FFI-safe error structure -#[repr(C)] -pub struct FfiError { - /// Error code - pub code: i32, - /// Error message (null-terminated, caller must free) - pub message: *mut libc::c_char, -} - -impl FfiError { - /// Create a success result - pub fn success() -> Self { - Self { - code: 0, - message: std::ptr::null_mut(), - } - } - - /// Create from IosError - pub fn from_error(err: &IosError) -> Self { - let code: FileProviderErrorCode = err.into(); - let message = CString::new(err.to_string()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()); - - Self { - code: code as i32, - message, - } - } -} - -/// Free an error message -/// -/// # Safety -/// The pointer must have been returned by a CFK function. -#[no_mangle] -pub unsafe extern "C" fn cfk_error_free(error: *mut FfiError) { - if !error.is_null() { - let err = &mut *error; - if !err.message.is_null() { - drop(CString::from_raw(err.message)); - err.message = std::ptr::null_mut(); - } - } -} diff --git a/czech-file-knife/cfk-ios/src/ffi.rs b/czech-file-knife/cfk-ios/src/ffi.rs deleted file mode 100644 index 0c3a42b19..000000000 --- a/czech-file-knife/cfk-ios/src/ffi.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! C FFI layer for iOS integration -//! -//! This module exposes a C API that can be called from Swift/Objective-C. - -use crate::domain::{DomainIdentifier, FileDomain}; -use crate::error::{FfiError, FileProviderErrorCode}; -use crate::item::{FileProviderItem, ItemIdentifier}; -use crate::provider::FileProviderManager; -use once_cell::sync::OnceCell; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::path::PathBuf; -use std::sync::Arc; - -/// Global provider manager -static MANAGER: OnceCell> = OnceCell::new(); - -/// Initialize the FFI layer -fn get_manager() -> Result<&'static Arc, FfiError> { - MANAGER - .get() - .ok_or_else(|| FfiError::from_error(&crate::error::IosError::Ffi( - "Manager not initialized".into(), - ))) -} - -// --- Initialization --- - -/// Initialize the provider manager -/// -/// # Safety -/// All string parameters must be valid null-terminated UTF-8 strings. -#[no_mangle] -pub unsafe extern "C" fn cfk_provider_init( - storage_path: *const c_char, - cache_path: *const c_char, - temp_path: *const c_char, -) -> i32 { - let storage = match CStr::from_ptr(storage_path).to_str() { - Ok(s) => PathBuf::from(s), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let cache = match CStr::from_ptr(cache_path).to_str() { - Ok(s) => PathBuf::from(s), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let temp = match CStr::from_ptr(temp_path).to_str() { - Ok(s) => PathBuf::from(s), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let manager = FileProviderManager::new(storage, cache, temp); - - // Initialize async - let rt = crate::runtime(); - if let Err(e) = rt.block_on(Arc::new(manager).initialize()) { - tracing::error!("Failed to initialize: {}", e); - return FileProviderErrorCode::Unknown as i32; - } - - let manager = FileProviderManager::new( - CStr::from_ptr(storage_path).to_str().unwrap_or(""), - CStr::from_ptr(cache_path).to_str().unwrap_or(""), - CStr::from_ptr(temp_path).to_str().unwrap_or(""), - ); - - let _ = MANAGER.set(Arc::new(manager)); - - FileProviderErrorCode::Success as i32 -} - -// --- Domain Management --- - -/// FFI-safe domain structure -#[repr(C)] -pub struct CfkDomain { - pub identifier: *mut c_char, - pub display_name: *mut c_char, - pub backend_type: *mut c_char, - pub enabled: bool, -} - -impl CfkDomain { - fn from_domain(domain: &FileDomain) -> Self { - Self { - identifier: CString::new(domain.identifier.0.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - display_name: CString::new(domain.display_name.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - backend_type: CString::new(domain.backend_type.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - enabled: domain.enabled, - } - } -} - -/// Free a domain structure -/// -/// # Safety -/// The pointer must have been returned by a CFK function. -#[no_mangle] -pub unsafe extern "C" fn cfk_domain_free(domain: *mut CfkDomain) { - if !domain.is_null() { - let d = &mut *domain; - if !d.identifier.is_null() { - drop(CString::from_raw(d.identifier)); - } - if !d.display_name.is_null() { - drop(CString::from_raw(d.display_name)); - } - if !d.backend_type.is_null() { - drop(CString::from_raw(d.backend_type)); - } - } -} - -/// Add a domain -/// -/// # Safety -/// All string parameters must be valid null-terminated UTF-8 strings. -#[no_mangle] -pub unsafe extern "C" fn cfk_domain_add( - identifier: *const c_char, - display_name: *const c_char, - backend_type: *const c_char, - config_json: *const c_char, -) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let id = match CStr::from_ptr(identifier).to_str() { - Ok(s) => s, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let name = match CStr::from_ptr(display_name).to_str() { - Ok(s) => s, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let backend = match CStr::from_ptr(backend_type).to_str() { - Ok(s) => s, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let config = if config_json.is_null() { - "{}".to_string() - } else { - CStr::from_ptr(config_json) - .to_str() - .unwrap_or("{}") - .to_string() - }; - - let domain = FileDomain::new(id, name, backend).with_config(config); - - let rt = crate::runtime(); - match rt.block_on(manager.add_domain(domain)) { - Ok(_) => FileProviderErrorCode::Success as i32, - Err(_) => FileProviderErrorCode::Unknown as i32, - } -} - -/// Remove a domain -/// -/// # Safety -/// The identifier must be a valid null-terminated UTF-8 string. -#[no_mangle] -pub unsafe extern "C" fn cfk_domain_remove(identifier: *const c_char) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let id = match CStr::from_ptr(identifier).to_str() { - Ok(s) => DomainIdentifier::new(s), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let rt = crate::runtime(); - match rt.block_on(manager.remove_domain(&id)) { - Ok(_) => FileProviderErrorCode::Success as i32, - Err(_) => FileProviderErrorCode::NoSuchItem as i32, - } -} - -// --- Item Operations --- - -/// FFI-safe item structure -#[repr(C)] -pub struct CfkItem { - pub identifier: *mut c_char, - pub parent_identifier: *mut c_char, - pub filename: *mut c_char, - pub item_type: u32, - pub size: u64, - pub has_size: bool, - pub capabilities: u64, - pub is_downloaded: bool, - pub is_uploaded: bool, -} - -impl CfkItem { - fn from_item(item: &FileProviderItem) -> Self { - Self { - identifier: CString::new(item.identifier.0.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - parent_identifier: CString::new(item.parent_identifier.0.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - filename: CString::new(item.filename.clone()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()), - item_type: item.item_type, - size: item.size.unwrap_or(0), - has_size: item.size.is_some(), - capabilities: item.capabilities, - is_downloaded: item.is_downloaded, - is_uploaded: item.is_uploaded, - } - } -} - -/// Free an item structure -/// -/// # Safety -/// The pointer must have been returned by a CFK function. -#[no_mangle] -pub unsafe extern "C" fn cfk_item_free(item: *mut CfkItem) { - if !item.is_null() { - let i = &mut *item; - if !i.identifier.is_null() { - drop(CString::from_raw(i.identifier)); - } - if !i.parent_identifier.is_null() { - drop(CString::from_raw(i.parent_identifier)); - } - if !i.filename.is_null() { - drop(CString::from_raw(i.filename)); - } - } -} - -/// Get item by identifier -/// -/// # Safety -/// The identifier must be a valid null-terminated UTF-8 string. -/// The caller must free the returned item with cfk_item_free. -#[no_mangle] -pub unsafe extern "C" fn cfk_item_get( - identifier: *const c_char, - out_item: *mut CfkItem, -) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let id = match CStr::from_ptr(identifier).to_str() { - Ok(s) => ItemIdentifier(s.to_string()), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let rt = crate::runtime(); - match rt.block_on(manager.item(&id)) { - Ok(item) => { - if !out_item.is_null() { - *out_item = CfkItem::from_item(&item); - } - FileProviderErrorCode::Success as i32 - } - Err(_) => FileProviderErrorCode::NoSuchItem as i32, - } -} - -/// Item list for enumeration -#[repr(C)] -pub struct CfkItemList { - pub items: *mut CfkItem, - pub count: usize, - pub next_page_token: *mut c_char, -} - -/// Free an item list -/// -/// # Safety -/// The pointer must have been returned by a CFK function. -#[no_mangle] -pub unsafe extern "C" fn cfk_item_list_free(list: *mut CfkItemList) { - if !list.is_null() { - let l = &mut *list; - if !l.items.is_null() { - let items = std::slice::from_raw_parts_mut(l.items, l.count); - for item in items { - cfk_item_free(item); - } - drop(Vec::from_raw_parts(l.items, l.count, l.count)); - } - if !l.next_page_token.is_null() { - drop(CString::from_raw(l.next_page_token)); - } - } -} - -/// Enumerate items in a container -/// -/// # Safety -/// All string parameters must be valid null-terminated UTF-8 strings. -#[no_mangle] -pub unsafe extern "C" fn cfk_enumerate_items( - container: *const c_char, - page_token: *const c_char, - out_list: *mut CfkItemList, -) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let container_id = match CStr::from_ptr(container).to_str() { - Ok(s) => ItemIdentifier(s.to_string()), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let token = if page_token.is_null() { - None - } else { - CStr::from_ptr(page_token).to_str().ok() - }; - - let rt = crate::runtime(); - match rt.block_on(manager.enumerate_items(&container_id, token)) { - Ok(page) => { - if !out_list.is_null() { - let items: Vec = page.items.iter().map(CfkItem::from_item).collect(); - let count = items.len(); - let ptr = items.as_ptr() as *mut CfkItem; - std::mem::forget(items); - - (*out_list).items = ptr; - (*out_list).count = count; - (*out_list).next_page_token = page - .next_page_token - .and_then(|t| CString::new(t).ok()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()); - } - FileProviderErrorCode::Success as i32 - } - Err(_) => FileProviderErrorCode::NoSuchItem as i32, - } -} - -/// Fetch file contents to local path -/// -/// # Safety -/// All string parameters must be valid null-terminated UTF-8 strings. -/// The caller must free the returned path string. -#[no_mangle] -pub unsafe extern "C" fn cfk_fetch_contents( - identifier: *const c_char, - out_path: *mut *mut c_char, -) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let id = match CStr::from_ptr(identifier).to_str() { - Ok(s) => ItemIdentifier(s.to_string()), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let rt = crate::runtime(); - match rt.block_on(manager.fetch_contents(&id)) { - Ok(path) => { - if !out_path.is_null() { - *out_path = CString::new(path.to_string_lossy().as_ref()) - .map(|s| s.into_raw()) - .unwrap_or(std::ptr::null_mut()); - } - FileProviderErrorCode::Success as i32 - } - Err(_) => FileProviderErrorCode::NoSuchItem as i32, - } -} - -/// Create a new item -/// -/// # Safety -/// All string parameters must be valid null-terminated UTF-8 strings. -#[no_mangle] -pub unsafe extern "C" fn cfk_create_item( - parent: *const c_char, - filename: *const c_char, - item_type: u32, - contents: *const u8, - contents_len: usize, - out_item: *mut CfkItem, -) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let parent_id = match CStr::from_ptr(parent).to_str() { - Ok(s) => ItemIdentifier(s.to_string()), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let name = match CStr::from_ptr(filename).to_str() { - Ok(s) => s, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let data = if contents.is_null() || contents_len == 0 { - None - } else { - Some(std::slice::from_raw_parts(contents, contents_len)) - }; - - let rt = crate::runtime(); - match rt.block_on(manager.create_item(&parent_id, name, item_type, data)) { - Ok(item) => { - if !out_item.is_null() { - *out_item = CfkItem::from_item(&item); - } - FileProviderErrorCode::Success as i32 - } - Err(_) => FileProviderErrorCode::Unknown as i32, - } -} - -/// Delete an item -/// -/// # Safety -/// The identifier must be a valid null-terminated UTF-8 string. -#[no_mangle] -pub unsafe extern "C" fn cfk_delete_item(identifier: *const c_char) -> i32 { - let manager = match get_manager() { - Ok(m) => m, - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let id = match CStr::from_ptr(identifier).to_str() { - Ok(s) => ItemIdentifier(s.to_string()), - Err(_) => return FileProviderErrorCode::Unknown as i32, - }; - - let rt = crate::runtime(); - match rt.block_on(manager.delete_item(&id)) { - Ok(_) => FileProviderErrorCode::Success as i32, - Err(_) => FileProviderErrorCode::NoSuchItem as i32, - } -} - -/// Free a string returned by CFK functions -/// -/// # Safety -/// The pointer must have been returned by a CFK function. -#[no_mangle] -pub unsafe extern "C" fn cfk_string_free(s: *mut c_char) { - if !s.is_null() { - drop(CString::from_raw(s)); - } -} diff --git a/czech-file-knife/cfk-ios/src/item.rs b/czech-file-knife/cfk-ios/src/item.rs deleted file mode 100644 index d7343fef6..000000000 --- a/czech-file-knife/cfk-ios/src/item.rs +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! File Provider Item representation -//! -//! Maps to NSFileProviderItem in iOS. - -use crate::domain::DomainIdentifier; -use crate::error::{IosError, IosResult}; -use cfk_core::{Entry, EntryKind, VirtualPath}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Item identifier (opaque string) -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct ItemIdentifier(pub String); - -impl ItemIdentifier { - /// Root container identifier - pub fn root() -> Self { - Self("root".to_string()) - } - - /// Working set identifier - pub fn working_set() -> Self { - Self(".workingset".to_string()) - } - - /// Trash identifier - pub fn trash() -> Self { - Self(".trash".to_string()) - } - - /// Create from domain and path - pub fn from_path(domain: &DomainIdentifier, path: &VirtualPath) -> Self { - Self(format!("{}:{}", domain.0, path)) - } - - /// Parse into domain and path - pub fn parse(&self) -> Option<(DomainIdentifier, String)> { - let parts: Vec<&str> = self.0.splitn(2, ':').collect(); - if parts.len() == 2 { - Some((DomainIdentifier::new(parts[0]), parts[1].to_string())) - } else { - None - } - } - - /// Check if this is a special identifier - pub fn is_special(&self) -> bool { - self.0.starts_with('.') - } - - /// Check if this is the root - pub fn is_root(&self) -> bool { - self.0 == "root" - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -/// Item type flags -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ItemType { - File = 0, - Directory = 1, - Symlink = 2, - Package = 3, // macOS/iOS package (folder displayed as file) - Unknown = 255, -} - -impl From for ItemType { - fn from(kind: EntryKind) -> Self { - match kind { - EntryKind::File => ItemType::File, - EntryKind::Directory => ItemType::Directory, - EntryKind::Symlink => ItemType::Symlink, - EntryKind::Unknown => ItemType::Unknown, - } - } -} - -/// Item capabilities (what operations are allowed) -#[derive(Debug, Clone, Copy)] -pub struct ItemCapabilities(pub u64); - -impl ItemCapabilities { - pub const READING: u64 = 1 << 0; - pub const WRITING: u64 = 1 << 1; - pub const REPARENTING: u64 = 1 << 2; // Can be moved - pub const RENAMING: u64 = 1 << 3; - pub const TRASHING: u64 = 1 << 4; - pub const DELETING: u64 = 1 << 5; - pub const EVICTING: u64 = 1 << 6; // Can be removed from local storage - pub const ADDING_SUBITEM: u64 = 1 << 7; - pub const CONTENT_ENUMERATION: u64 = 1 << 8; - pub const PLAYING: u64 = 1 << 9; - - /// Default capabilities for a file - pub fn file_default() -> Self { - Self( - Self::READING - | Self::WRITING - | Self::REPARENTING - | Self::RENAMING - | Self::TRASHING - | Self::DELETING - | Self::EVICTING, - ) - } - - /// Default capabilities for a directory - pub fn directory_default() -> Self { - Self( - Self::READING - | Self::REPARENTING - | Self::RENAMING - | Self::TRASHING - | Self::DELETING - | Self::ADDING_SUBITEM - | Self::CONTENT_ENUMERATION, - ) - } - - /// Read-only capabilities - pub fn read_only() -> Self { - Self(Self::READING | Self::EVICTING | Self::CONTENT_ENUMERATION) - } -} - -/// File Provider Item -/// -/// Represents a file or folder in the File Provider. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileProviderItem { - /// Unique identifier - pub identifier: ItemIdentifier, - /// Parent identifier - pub parent_identifier: ItemIdentifier, - /// Filename (not full path) - pub filename: String, - /// Item type - pub item_type: u32, - /// File size (for files) - pub size: Option, - /// Creation date - pub creation_date: Option>, - /// Modification date - pub content_modification_date: Option>, - /// Content type (UTI) - pub content_type: Option, - /// Capabilities - pub capabilities: u64, - /// Whether downloaded - pub is_downloaded: bool, - /// Whether downloading - pub is_downloading: bool, - /// Whether uploaded - pub is_uploaded: bool, - /// Whether uploading - pub is_uploading: bool, - /// Download progress (0.0 - 1.0) - pub download_progress: f64, - /// Upload progress (0.0 - 1.0) - pub upload_progress: f64, - /// Version identifier (for conflict resolution) - pub version_identifier: Option, - /// Content checksum - pub checksum: Option, - /// Favorite status - pub is_favorite: bool, - /// Tag data - pub tag_data: Option>, - /// Custom metadata - #[serde(default)] - pub user_info: HashMap, -} - -impl FileProviderItem { - /// Create from cfk_core Entry - pub fn from_entry( - domain: &DomainIdentifier, - entry: &Entry, - parent: &ItemIdentifier, - ) -> Self { - let filename = entry - .path - .segments - .last() - .cloned() - .unwrap_or_else(|| entry.path.backend.clone()); - - let item_type: ItemType = entry.kind.into(); - let capabilities = match entry.kind { - EntryKind::Directory => ItemCapabilities::directory_default(), - _ => ItemCapabilities::file_default(), - }; - - let content_type = entry.metadata.mime_type.clone().or_else(|| { - // Guess from filename - if filename.ends_with(".txt") { - Some("public.plain-text".to_string()) - } else if filename.ends_with(".pdf") { - Some("com.adobe.pdf".to_string()) - } else if filename.ends_with(".jpg") || filename.ends_with(".jpeg") { - Some("public.jpeg".to_string()) - } else if filename.ends_with(".png") { - Some("public.png".to_string()) - } else { - Some("public.data".to_string()) - } - }); - - Self { - identifier: ItemIdentifier::from_path(domain, &entry.path), - parent_identifier: parent.clone(), - filename, - item_type: item_type as u32, - size: entry.metadata.size, - creation_date: entry.metadata.created, - content_modification_date: entry.metadata.modified, - content_type, - capabilities: capabilities.0, - is_downloaded: false, - is_downloading: false, - is_uploaded: true, - is_uploading: false, - download_progress: 0.0, - upload_progress: 1.0, - version_identifier: entry.metadata.content_hash.clone(), - checksum: entry.metadata.content_hash.clone(), - is_favorite: false, - tag_data: None, - user_info: entry.metadata.custom.clone(), - } - } - - /// Create a root item - pub fn root(domain: &DomainIdentifier, display_name: &str) -> Self { - Self { - identifier: ItemIdentifier::root(), - parent_identifier: ItemIdentifier::root(), - filename: display_name.to_string(), - item_type: ItemType::Directory as u32, - size: None, - creation_date: None, - content_modification_date: None, - content_type: Some("public.folder".to_string()), - capabilities: ItemCapabilities::directory_default().0, - is_downloaded: true, - is_downloading: false, - is_uploaded: true, - is_uploading: false, - download_progress: 1.0, - upload_progress: 1.0, - version_identifier: None, - checksum: None, - is_favorite: false, - tag_data: None, - user_info: HashMap::new(), - } - } - - /// Check if this is a directory - pub fn is_directory(&self) -> bool { - self.item_type == ItemType::Directory as u32 - } - - /// Check if this is a file - pub fn is_file(&self) -> bool { - self.item_type == ItemType::File as u32 - } - - /// Set download state - pub fn set_downloading(&mut self, progress: f64) { - self.is_downloading = progress < 1.0; - self.is_downloaded = progress >= 1.0; - self.download_progress = progress; - } - - /// Set upload state - pub fn set_uploading(&mut self, progress: f64) { - self.is_uploading = progress < 1.0; - self.is_uploaded = progress >= 1.0; - self.upload_progress = progress; - } -} - -/// Item version for conflict resolution -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ItemVersion { - /// Content version (changes when file content changes) - pub content_version: Vec, - /// Metadata version (changes when metadata changes) - pub metadata_version: Vec, -} - -impl ItemVersion { - pub fn new(content: impl AsRef<[u8]>, metadata: impl AsRef<[u8]>) -> Self { - Self { - content_version: content.as_ref().to_vec(), - metadata_version: metadata.as_ref().to_vec(), - } - } - - pub fn from_checksum(checksum: &str) -> Self { - Self { - content_version: checksum.as_bytes().to_vec(), - metadata_version: checksum.as_bytes().to_vec(), - } - } -} - -/// Enumeration page for paginated listing -#[derive(Debug, Clone)] -pub struct EnumerationPage { - pub items: Vec, - pub next_page_token: Option, - pub sync_anchor: Option>, -} - -impl EnumerationPage { - pub fn new(items: Vec) -> Self { - Self { - items, - next_page_token: None, - sync_anchor: None, - } - } - - pub fn with_next_page(mut self, token: String) -> Self { - self.next_page_token = Some(token); - self - } - - pub fn with_sync_anchor(mut self, anchor: Vec) -> Self { - self.sync_anchor = Some(anchor); - self - } -} diff --git a/czech-file-knife/cfk-ios/src/lib.rs b/czech-file-knife/cfk-ios/src/lib.rs deleted file mode 100644 index 80f1758df..000000000 --- a/czech-file-knife/cfk-ios/src/lib.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! iOS File Provider extension for Czech File Knife -//! -//! This crate provides iOS integration via Apple's File Provider framework. -//! It exposes a C FFI layer that can be called from Swift/Objective-C. -//! -//! # Architecture -//! -//! ```text -//! ┌─────────────────────────────────────────────────────────────┐ -//! │ iOS App / Extension │ -//! ├─────────────────────────────────────────────────────────────┤ -//! │ Swift File Provider │ -//! │ (CfkFileProviderExtension.swift) │ -//! ├─────────────────────────────────────────────────────────────┤ -//! │ C FFI Bridge Layer │ -//! │ (ffi.rs + CfkBridge.h) │ -//! ├─────────────────────────────────────────────────────────────┤ -//! │ Rust Core Library │ -//! │ (cfk-core, cfk-providers, cfk-cache) │ -//! └─────────────────────────────────────────────────────────────┘ -//! ``` -//! -//! # Usage -//! -//! 1. Build as a static library for iOS targets -//! 2. Link with your File Provider extension -//! 3. Use the Swift wrapper classes - -#![allow(dead_code)] // FFI functions may not be called from Rust - -pub mod domain; -pub mod error; -pub mod ffi; -pub mod item; -pub mod provider; - -pub use domain::FileDomain; -pub use error::{IosError, IosResult}; -pub use item::{FileProviderItem, ItemIdentifier}; -pub use provider::FileProviderManager; - -use once_cell::sync::OnceCell; -use std::sync::Arc; -use tokio::runtime::Runtime; - -/// Global Tokio runtime for async operations -static RUNTIME: OnceCell = OnceCell::new(); - -/// Initialize the global runtime -pub fn init_runtime() -> &'static Runtime { - RUNTIME.get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - .expect("Failed to create Tokio runtime") - }) -} - -/// Get the global runtime -pub fn runtime() -> &'static Runtime { - RUNTIME.get().expect("Runtime not initialized") -} - -/// Initialize the iOS integration -/// -/// Must be called before any other FFI functions. -#[no_mangle] -pub extern "C" fn cfk_ios_init() -> i32 { - // Initialize tracing for iOS - #[cfg(debug_assertions)] - { - use tracing_subscriber::prelude::*; - let _ = tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .try_init(); - } - - // Initialize runtime - let _ = init_runtime(); - - tracing::info!("CFK iOS initialized"); - 0 -} - -/// Shutdown the iOS integration -#[no_mangle] -pub extern "C" fn cfk_ios_shutdown() { - tracing::info!("CFK iOS shutting down"); - // Runtime will be dropped when the process exits -} diff --git a/czech-file-knife/cfk-ios/src/provider.rs b/czech-file-knife/cfk-ios/src/provider.rs deleted file mode 100644 index d25bd78fc..000000000 --- a/czech-file-knife/cfk-ios/src/provider.rs +++ /dev/null @@ -1,499 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! File Provider Manager -//! -//! Coordinates between iOS File Provider and CFK backends. - -use crate::domain::{DomainIdentifier, DomainManager, FileDomain}; -use crate::error::{IosError, IosResult}; -use crate::item::{EnumerationPage, FileProviderItem, ItemIdentifier}; -use bytes::Bytes; -use cfk_core::backend::{ByteStream, SpaceInfo}; -use cfk_core::entry::DirectoryListing; -use cfk_core::operations::{ - CopyOptions, DeleteOptions, ListOptions, MoveOptions, ReadOptions, WriteOptions, -}; -use cfk_core::{Entry, StorageBackend, StorageCapabilities, VirtualPath}; -use futures::StreamExt; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Placeholder backend for when real backends aren't available -struct PlaceholderBackend { - id: String, -} - -#[async_trait::async_trait] -impl StorageBackend for PlaceholderBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Placeholder" - } - - fn capabilities(&self) -> &StorageCapabilities { - static CAPS: StorageCapabilities = StorageCapabilities { - read: false, - write: false, - delete: false, - rename: false, - copy: false, - list: false, - search: false, - versioning: false, - sharing: false, - offline: false, - streaming: false, - resumable_uploads: false, - content_hashing: false, - }; - &CAPS - } - - async fn is_available(&self) -> bool { - false - } - - async fn get_metadata(&self, _path: &VirtualPath) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn list_directory( - &self, - _path: &VirtualPath, - _options: &ListOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn read_file( - &self, - _path: &VirtualPath, - _options: &ReadOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn write_file( - &self, - _path: &VirtualPath, - _data: Bytes, - _options: &WriteOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn write_file_stream( - &self, - _path: &VirtualPath, - _stream: ByteStream, - _size_hint: Option, - _options: &WriteOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn delete( - &self, - _path: &VirtualPath, - _options: &DeleteOptions, - ) -> cfk_core::CfkResult<()> { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn create_directory(&self, _path: &VirtualPath) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn copy( - &self, - _from: &VirtualPath, - _to: &VirtualPath, - _options: &CopyOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn rename( - &self, - _from: &VirtualPath, - _to: &VirtualPath, - _options: &MoveOptions, - ) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } - - async fn get_space_info(&self) -> cfk_core::CfkResult { - Err(cfk_core::CfkError::Unsupported("Placeholder backend".into())) - } -} - -/// File Provider Manager -/// -/// Main entry point for iOS File Provider operations. -pub struct FileProviderManager { - /// Domain manager - domains: Arc, - /// Active backends - backends: Arc>>>, - /// Local cache directory - cache_dir: PathBuf, - /// Temporary file directory - temp_dir: PathBuf, -} - -impl FileProviderManager { - /// Create a new manager - pub fn new( - storage_path: impl Into, - cache_dir: impl Into, - temp_dir: impl Into, - ) -> Self { - Self { - domains: Arc::new(DomainManager::new(storage_path)), - backends: Arc::new(RwLock::new(HashMap::new())), - cache_dir: cache_dir.into(), - temp_dir: temp_dir.into(), - } - } - - /// Initialize the manager - pub async fn initialize(&self) -> IosResult<()> { - // Load saved domains - self.domains.load().await?; - - // Create cache/temp directories - tokio::fs::create_dir_all(&self.cache_dir) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - tokio::fs::create_dir_all(&self.temp_dir) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - // Initialize backends for enabled domains - for domain in self.domains.list_enabled().await { - if let Err(e) = self.init_backend(&domain).await { - tracing::warn!("Failed to init backend for {}: {}", domain.identifier.0, e); - } - } - - Ok(()) - } - - /// Initialize a backend for a domain - async fn init_backend(&self, domain: &FileDomain) -> IosResult<()> { - // In a full implementation, this would create the appropriate backend - // based on domain.backend_type and domain.config_json - let backend: Arc = Arc::new(PlaceholderBackend { - id: domain.identifier.0.clone(), - }); - - self.backends - .write() - .await - .insert(domain.identifier.clone(), backend); - - Ok(()) - } - - /// Get backend for a domain - async fn get_backend( - &self, - domain_id: &DomainIdentifier, - ) -> IosResult> { - self.backends - .read() - .await - .get(domain_id) - .cloned() - .ok_or_else(|| IosError::NotFound(format!("Backend not found: {}", domain_id.0))) - } - - // --- Domain Management --- - - /// Add a new domain - pub async fn add_domain(&self, domain: FileDomain) -> IosResult<()> { - self.domains.add(domain.clone()).await?; - if domain.enabled { - self.init_backend(&domain).await?; - } - Ok(()) - } - - /// Remove a domain - pub async fn remove_domain(&self, id: &DomainIdentifier) -> IosResult<()> { - self.domains.remove(id).await?; - self.backends.write().await.remove(id); - Ok(()) - } - - /// List all domains - pub async fn list_domains(&self) -> Vec { - self.domains.list().await - } - - // --- Item Operations --- - - /// Get item for identifier - pub async fn item(&self, identifier: &ItemIdentifier) -> IosResult { - if identifier.is_root() { - // Return root item - return Ok(FileProviderItem::root( - &DomainIdentifier::new("default"), - "Root", - )); - } - - let (domain_id, path_str) = identifier - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(identifier.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - let entry = backend - .get_metadata(&path) - .await - .map_err(IosError::Core)?; - - // Determine parent - let parent = if path.segments.is_empty() { - ItemIdentifier::root() - } else { - let mut parent_path = path.clone(); - parent_path.segments.pop(); - ItemIdentifier::from_path(&domain_id, &parent_path) - }; - - Ok(FileProviderItem::from_entry(&domain_id, &entry, &parent)) - } - - /// Enumerate items in a container - pub async fn enumerate_items( - &self, - container: &ItemIdentifier, - _page_token: Option<&str>, - ) -> IosResult { - if container.is_root() { - // List domains as root items - let domains = self.domains.list_enabled().await; - let items: Vec = domains - .iter() - .map(|d| { - let mut item = FileProviderItem::root(&d.identifier, &d.display_name); - item.identifier = ItemIdentifier(d.identifier.0.clone() + ":/"); - item.parent_identifier = ItemIdentifier::root(); - item - }) - .collect(); - - return Ok(EnumerationPage::new(items)); - } - - let (domain_id, path_str) = container - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(container.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - let listing = backend - .list_directory(&path, &ListOptions::default()) - .await - .map_err(IosError::Core)?; - - let items: Vec = listing - .entries - .iter() - .map(|e| FileProviderItem::from_entry(&domain_id, e, container)) - .collect(); - - Ok(EnumerationPage::new(items)) - } - - /// Fetch contents of a file - pub async fn fetch_contents(&self, identifier: &ItemIdentifier) -> IosResult { - let (domain_id, path_str) = identifier - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(identifier.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - let mut stream = backend - .read_file(&path, &ReadOptions::default()) - .await - .map_err(IosError::Core)?; - - // Collect stream into bytes - let mut data = Vec::new(); - while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result.map_err(IosError::Core)?; - data.extend_from_slice(&chunk); - } - - // Write to cache - let cache_path = self.cache_dir.join(&identifier.0.replace([':', '/'], "_")); - tokio::fs::write(&cache_path, &data) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - - Ok(cache_path) - } - - /// Create a new item - pub async fn create_item( - &self, - parent: &ItemIdentifier, - filename: &str, - item_type: u32, - contents: Option<&[u8]>, - ) -> IosResult { - let (domain_id, parent_path_str) = parent - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(parent.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let parent_path = - VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, parent_path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &parent_path_str)); - - let item_path = parent_path.join(filename); - - let entry = if item_type == 1 { - // Directory - backend - .create_directory(&item_path) - .await - .map_err(IosError::Core)? - } else { - // File - let data = contents.map(Bytes::copy_from_slice).unwrap_or_default(); - backend - .write_file(&item_path, data, &WriteOptions::default()) - .await - .map_err(IosError::Core)? - }; - - Ok(FileProviderItem::from_entry(&domain_id, &entry, parent)) - } - - /// Modify item contents - pub async fn modify_item( - &self, - identifier: &ItemIdentifier, - contents: &[u8], - ) -> IosResult { - let (domain_id, path_str) = identifier - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(identifier.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - let entry = backend - .write_file(&path, Bytes::copy_from_slice(contents), &WriteOptions::default()) - .await - .map_err(IosError::Core)?; - - // Determine parent - let parent = if path.segments.len() <= 1 { - ItemIdentifier::root() - } else { - let mut parent_path = path.clone(); - parent_path.segments.pop(); - ItemIdentifier::from_path(&domain_id, &parent_path) - }; - - Ok(FileProviderItem::from_entry(&domain_id, &entry, &parent)) - } - - /// Delete an item - pub async fn delete_item(&self, identifier: &ItemIdentifier) -> IosResult<()> { - let (domain_id, path_str) = identifier - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(identifier.0.clone()))?; - - let backend = self.get_backend(&domain_id).await?; - let path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - backend - .delete(&path, &DeleteOptions::default()) - .await - .map_err(IosError::Core)?; - - // Remove from cache - let cache_path = self.cache_dir.join(&identifier.0.replace([':', '/'], "_")); - let _ = tokio::fs::remove_file(&cache_path).await; - - Ok(()) - } - - /// Rename/move an item - pub async fn reparent_item( - &self, - identifier: &ItemIdentifier, - new_parent: &ItemIdentifier, - new_name: Option<&str>, - ) -> IosResult { - let (domain_id, path_str) = identifier - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(identifier.0.clone()))?; - - let (new_domain_id, new_parent_path_str) = new_parent - .parse() - .ok_or_else(|| IosError::InvalidIdentifier(new_parent.0.clone()))?; - - if domain_id != new_domain_id { - return Err(IosError::NotSupported( - "Cross-domain move not supported".into(), - )); - } - - let backend = self.get_backend(&domain_id).await?; - let from_path = VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &path_str)); - - let new_parent_path = - VirtualPath::parse_uri(&format!("cfk://{}/{}", domain_id.0, new_parent_path_str)) - .unwrap_or_else(|| VirtualPath::new(&domain_id.0, &new_parent_path_str)); - - let new_name = new_name.unwrap_or_else(|| { - from_path.segments.last().map(|s| s.as_str()).unwrap_or("") - }); - - let to_path = new_parent_path.join(new_name); - - let entry = backend - .rename(&from_path, &to_path, &MoveOptions::default()) - .await - .map_err(IosError::Core)?; - - Ok(FileProviderItem::from_entry(&domain_id, &entry, new_parent)) - } - - /// Get storage space info for a domain - pub async fn space_info(&self, domain_id: &DomainIdentifier) -> IosResult<(u64, u64)> { - let backend = self.get_backend(domain_id).await?; - let info = backend.get_space_info().await.map_err(IosError::Core)?; - Ok((info.total.unwrap_or(0), info.used.unwrap_or(0))) - } - - /// Evict item from local cache - pub async fn evict_item(&self, identifier: &ItemIdentifier) -> IosResult<()> { - let cache_path = self.cache_dir.join(&identifier.0.replace([':', '/'], "_")); - tokio::fs::remove_file(&cache_path) - .await - .map_err(|e| IosError::Core(cfk_core::CfkError::Io(e)))?; - Ok(()) - } -} diff --git a/czech-file-knife/cfk-ios/swift/CfkBridge.h b/czech-file-knife/cfk-ios/swift/CfkBridge.h deleted file mode 100644 index b1211ccc2..000000000 --- a/czech-file-knife/cfk-ios/swift/CfkBridge.h +++ /dev/null @@ -1,239 +0,0 @@ -/* - * CfkBridge.h - C bridging header for Czech File Knife iOS integration - * - * This header exposes the Rust FFI functions to Swift/Objective-C. - * Include this in your File Provider extension's bridging header. - */ - -#ifndef CFK_BRIDGE_H -#define CFK_BRIDGE_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* --- Initialization --- */ - -/** - * Initialize the CFK iOS runtime. - * Must be called before any other CFK functions. - * Returns 0 on success, negative on error. - */ -int32_t cfk_ios_init(void); - -/** - * Shutdown the CFK iOS runtime. - * Call when the extension is terminating. - */ -void cfk_ios_shutdown(void); - -/** - * Initialize the provider manager. - * @param storage_path Path to store domain configuration - * @param cache_path Path for cached file contents - * @param temp_path Path for temporary files - * Returns 0 on success. - */ -int32_t cfk_provider_init( - const char *storage_path, - const char *cache_path, - const char *temp_path -); - -/* --- Error Handling --- */ - -/** - * Error structure returned by CFK functions. - */ -typedef struct { - int32_t code; /* Error code (0 = success) */ - char *message; /* Error message (caller must free) */ -} CfkError; - -/** - * Free an error structure. - */ -void cfk_error_free(CfkError *error); - -/** - * Free a string returned by CFK functions. - */ -void cfk_string_free(char *s); - -/* --- Domain Management --- */ - -/** - * Domain information structure. - */ -typedef struct { - char *identifier; /* Unique domain ID */ - char *display_name; /* Name shown in Files app */ - char *backend_type; /* Backend type (dropbox, gdrive, etc.) */ - bool enabled; /* Whether domain is enabled */ -} CfkDomain; - -/** - * Free a domain structure. - */ -void cfk_domain_free(CfkDomain *domain); - -/** - * Add a new domain. - * @param identifier Unique identifier for the domain - * @param display_name Name shown in Files app - * @param backend_type Type of backend (dropbox, gdrive, onedrive, etc.) - * @param config_json JSON configuration for the backend (can be NULL) - * Returns 0 on success. - */ -int32_t cfk_domain_add( - const char *identifier, - const char *display_name, - const char *backend_type, - const char *config_json -); - -/** - * Remove a domain. - * @param identifier Domain identifier to remove - * Returns 0 on success. - */ -int32_t cfk_domain_remove(const char *identifier); - -/* --- Item Operations --- */ - -/** - * Item information structure. - */ -typedef struct { - char *identifier; /* Unique item identifier */ - char *parent_identifier; /* Parent item identifier */ - char *filename; /* File/folder name */ - uint32_t item_type; /* 0=file, 1=directory, 2=symlink */ - uint64_t size; /* File size in bytes */ - bool has_size; /* Whether size is valid */ - uint64_t capabilities; /* Capability flags */ - bool is_downloaded; /* Whether content is cached locally */ - bool is_uploaded; /* Whether content is synced to server */ -} CfkItem; - -/** - * Free an item structure. - */ -void cfk_item_free(CfkItem *item); - -/** - * Get item by identifier. - * @param identifier Item identifier - * @param out_item Output item structure - * Returns 0 on success. - */ -int32_t cfk_item_get( - const char *identifier, - CfkItem *out_item -); - -/** - * Item list for enumeration results. - */ -typedef struct { - CfkItem *items; /* Array of items */ - size_t count; /* Number of items */ - char *next_page_token; /* Token for next page (NULL if no more) */ -} CfkItemList; - -/** - * Free an item list. - */ -void cfk_item_list_free(CfkItemList *list); - -/** - * Enumerate items in a container. - * @param container Container identifier (use "root" for root) - * @param page_token Page token for pagination (can be NULL) - * @param out_list Output item list - * Returns 0 on success. - */ -int32_t cfk_enumerate_items( - const char *container, - const char *page_token, - CfkItemList *out_list -); - -/** - * Fetch file contents to local storage. - * @param identifier File identifier - * @param out_path Output path where file was downloaded - * Returns 0 on success. - */ -int32_t cfk_fetch_contents( - const char *identifier, - char **out_path -); - -/** - * Create a new item. - * @param parent Parent container identifier - * @param filename Name of the new item - * @param item_type Type (0=file, 1=directory) - * @param contents File contents (can be NULL for directories) - * @param contents_len Length of contents - * @param out_item Output item structure - * Returns 0 on success. - */ -int32_t cfk_create_item( - const char *parent, - const char *filename, - uint32_t item_type, - const uint8_t *contents, - size_t contents_len, - CfkItem *out_item -); - -/** - * Delete an item. - * @param identifier Item identifier - * Returns 0 on success. - */ -int32_t cfk_delete_item(const char *identifier); - -/* --- Error Codes (matching NSFileProviderErrorCode) --- */ - -#define CFK_ERROR_SUCCESS 0 -#define CFK_ERROR_NO_SUCH_ITEM -1000 -#define CFK_ERROR_ITEM_ALREADY_EXISTS -1001 -#define CFK_ERROR_NOT_AUTHENTICATED -1002 -#define CFK_ERROR_SERVER_UNREACHABLE -1003 -#define CFK_ERROR_QUOTA_EXCEEDED -1004 -#define CFK_ERROR_FILENAME_INVALID -1005 -#define CFK_ERROR_VERSION_OUT_OF_DATE -1006 -#define CFK_ERROR_CANNOT_SYNC -1010 -#define CFK_ERROR_UNKNOWN -9999 - -/* --- Item Capabilities --- */ - -#define CFK_CAP_READING (1ULL << 0) -#define CFK_CAP_WRITING (1ULL << 1) -#define CFK_CAP_REPARENTING (1ULL << 2) -#define CFK_CAP_RENAMING (1ULL << 3) -#define CFK_CAP_TRASHING (1ULL << 4) -#define CFK_CAP_DELETING (1ULL << 5) -#define CFK_CAP_EVICTING (1ULL << 6) -#define CFK_CAP_ADDING_SUBITEM (1ULL << 7) -#define CFK_CAP_CONTENT_ENUMERATION (1ULL << 8) -#define CFK_CAP_PLAYING (1ULL << 9) - -/* --- Item Types --- */ - -#define CFK_ITEM_TYPE_FILE 0 -#define CFK_ITEM_TYPE_DIRECTORY 1 -#define CFK_ITEM_TYPE_SYMLINK 2 -#define CFK_ITEM_TYPE_PACKAGE 3 - -#ifdef __cplusplus -} -#endif - -#endif /* CFK_BRIDGE_H */ diff --git a/czech-file-knife/cfk-ios/swift/CfkFileProviderExtension.swift b/czech-file-knife/cfk-ios/swift/CfkFileProviderExtension.swift deleted file mode 100644 index 4d3a0aeb5..000000000 --- a/czech-file-knife/cfk-ios/swift/CfkFileProviderExtension.swift +++ /dev/null @@ -1,337 +0,0 @@ -/* - * CfkFileProviderExtension.swift - File Provider Extension implementation - * - * This class implements NSFileProviderReplicatedExtension for full - * file provider support in iOS/macOS. - */ - -import FileProvider -import UniformTypeIdentifiers - -/// Czech File Knife File Provider Extension -@available(iOS 16.0, macOS 13.0, *) -open class CfkFileProviderExtension: NSObject, NSFileProviderReplicatedExtension { - - // MARK: - Properties - - /// The domain this extension is serving - public let domain: NSFileProviderDomain - - /// Manager instance (initialized lazily) - private var isInitialized = false - - // MARK: - Initialization - - public required init(domain: NSFileProviderDomain) { - self.domain = domain - super.init() - } - - /// Initialize the CFK backend - private func ensureInitialized() throws { - guard !isInitialized else { return } - - let containerURL = NSFileProviderManager(for: domain)?.documentStorageURL - ?? FileManager.default.temporaryDirectory - - let storageURL = containerURL.appendingPathComponent("cfk-storage") - let cacheURL = containerURL.appendingPathComponent("cfk-cache") - let tempURL = containerURL.appendingPathComponent("cfk-temp") - - // Create directories - try FileManager.default.createDirectory(at: storageURL, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true) - - try initializeCfk(storagePath: storageURL, cachePath: cacheURL, tempPath: tempURL) - isInitialized = true - } - - // MARK: - NSFileProviderReplicatedExtension - - public func invalidate() { - shutdownCfk() - isInitialized = false - } - - public func item( - for identifier: NSFileProviderItemIdentifier, - request: NSFileProviderRequest, - completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void - ) -> Progress { - let progress = Progress(totalUnitCount: 1) - - Task { - do { - try ensureInitialized() - - let id = identifier == .rootContainer ? "root" : identifier.rawValue - let item = try CfkFileProviderItem.fetch(identifier: id) - - progress.completedUnitCount = 1 - completionHandler(item, nil) - } catch { - completionHandler(nil, error) - } - } - - return progress - } - - public func fetchContents( - for itemIdentifier: NSFileProviderItemIdentifier, - version requestedVersion: NSFileProviderItemVersion?, - request: NSFileProviderRequest, - completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void - ) -> Progress { - let progress = Progress(totalUnitCount: 100) - - Task { - do { - try ensureInitialized() - - var pathPtr: UnsafeMutablePointer? - let result = itemIdentifier.rawValue.withCString { id in - cfk_fetch_contents(id, &pathPtr) - } - - guard result == CFK_ERROR_SUCCESS, let path = pathPtr else { - throw CfkError.from(code: result) - } - - let localPath = URL(fileURLWithPath: String(cString: path)) - cfk_string_free(pathPtr) - - let item = try CfkFileProviderItem.fetch(identifier: itemIdentifier.rawValue) - - progress.completedUnitCount = 100 - completionHandler(localPath, item, nil) - } catch { - completionHandler(nil, nil, error) - } - } - - return progress - } - - public func createItem( - basedOn itemTemplate: NSFileProviderItem, - fields: NSFileProviderItemFields, - contents url: URL?, - options: NSFileProviderCreateItemOptions = [], - request: NSFileProviderRequest, - completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void - ) -> Progress { - let progress = Progress(totalUnitCount: 100) - - Task { - do { - try ensureInitialized() - - let parentId = itemTemplate.parentItemIdentifier == .rootContainer - ? "root" - : itemTemplate.parentItemIdentifier.rawValue - let filename = itemTemplate.filename - let isDirectory = itemTemplate.contentType == .folder - - // Read contents if file - var contentsData: Data? - if let url = url, !isDirectory { - contentsData = try Data(contentsOf: url) - } - - var cItem = CfkItem() - let result: Int32 = parentId.withCString { parent in - filename.withCString { name in - if let data = contentsData { - return data.withUnsafeBytes { bytes in - cfk_create_item( - parent, - name, - isDirectory ? UInt32(CFK_ITEM_TYPE_DIRECTORY) : UInt32(CFK_ITEM_TYPE_FILE), - bytes.baseAddress?.assumingMemoryBound(to: UInt8.self), - data.count, - &cItem - ) - } - } else { - return cfk_create_item( - parent, - name, - isDirectory ? UInt32(CFK_ITEM_TYPE_DIRECTORY) : UInt32(CFK_ITEM_TYPE_FILE), - nil, - 0, - &cItem - ) - } - } - } - - guard result == CFK_ERROR_SUCCESS else { - throw CfkError.from(code: result) - } - - defer { cfk_item_free(&cItem) } - let item = CfkFileProviderItem(from: cItem) - - progress.completedUnitCount = 100 - completionHandler(item, [], false, nil) - } catch { - completionHandler(nil, [], false, error) - } - } - - return progress - } - - public func modifyItem( - _ item: NSFileProviderItem, - baseVersion version: NSFileProviderItemVersion, - changedFields: NSFileProviderItemFields, - contents newContents: URL?, - options: NSFileProviderModifyItemOptions = [], - request: NSFileProviderRequest, - completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void - ) -> Progress { - let progress = Progress(totalUnitCount: 100) - - Task { - do { - try ensureInitialized() - - // For now, just refetch the item - let updated = try CfkFileProviderItem.fetch(identifier: item.itemIdentifier.rawValue) - - progress.completedUnitCount = 100 - completionHandler(updated, [], false, nil) - } catch { - completionHandler(nil, [], false, error) - } - } - - return progress - } - - public func deleteItem( - identifier: NSFileProviderItemIdentifier, - baseVersion version: NSFileProviderItemVersion, - options: NSFileProviderDeleteItemOptions = [], - request: NSFileProviderRequest, - completionHandler: @escaping (Error?) -> Void - ) -> Progress { - let progress = Progress(totalUnitCount: 1) - - Task { - do { - try ensureInitialized() - - let result = identifier.rawValue.withCString { id in - cfk_delete_item(id) - } - - guard result == CFK_ERROR_SUCCESS else { - throw CfkError.from(code: result) - } - - progress.completedUnitCount = 1 - completionHandler(nil) - } catch { - completionHandler(error) - } - } - - return progress - } - - // MARK: - Enumeration - - public func enumerator( - for containerItemIdentifier: NSFileProviderItemIdentifier, - request: NSFileProviderRequest - ) throws -> NSFileProviderEnumerator { - try ensureInitialized() - return CfkEnumerator(containerIdentifier: containerItemIdentifier) - } -} - -// MARK: - Enumerator - -@available(iOS 16.0, macOS 13.0, *) -class CfkEnumerator: NSObject, NSFileProviderEnumerator { - - let containerIdentifier: NSFileProviderItemIdentifier - - init(containerIdentifier: NSFileProviderItemIdentifier) { - self.containerIdentifier = containerIdentifier - super.init() - } - - func invalidate() { - // Clean up if needed - } - - func enumerateItems( - for observer: NSFileProviderEnumerationObserver, - startingAt page: NSFileProviderPage - ) { - let containerId = containerIdentifier == .rootContainer - ? "root" - : containerIdentifier.rawValue - - var itemList = CfkItemList() - let pageToken: String? = page == NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage - || page == NSFileProviderPage.initialPageSortedByName as NSFileProviderPage - ? nil - : String(data: page.rawValue, encoding: .utf8) - - let result: Int32 = containerId.withCString { container in - if let token = pageToken { - return token.withCString { tokenPtr in - cfk_enumerate_items(container, tokenPtr, &itemList) - } - } else { - return cfk_enumerate_items(container, nil, &itemList) - } - } - - guard result == CFK_ERROR_SUCCESS else { - observer.finishEnumeratingWithError(CfkError.from(code: result).fileProviderError) - return - } - - defer { cfk_item_list_free(&itemList) } - - // Convert items - var items: [CfkFileProviderItem] = [] - if let itemsPtr = itemList.items { - for i in 0.. Void) { - // Return a simple anchor based on current time - let anchor = NSFileProviderSyncAnchor(Date().timeIntervalSince1970.description.data(using: .utf8)!) - completionHandler(anchor) - } -} diff --git a/czech-file-knife/cfk-ios/swift/CfkFileProviderItem.swift b/czech-file-knife/cfk-ios/swift/CfkFileProviderItem.swift deleted file mode 100644 index 43c4721bb..000000000 --- a/czech-file-knife/cfk-ios/swift/CfkFileProviderItem.swift +++ /dev/null @@ -1,243 +0,0 @@ -/* - * CfkFileProviderItem.swift - NSFileProviderItem implementation - * - * This class wraps CfkItem from the Rust FFI layer and implements - * the NSFileProviderItem protocol for use in File Provider extensions. - */ - -import FileProvider -import UniformTypeIdentifiers - -/// File Provider Item backed by CFK Rust library -@available(iOS 16.0, macOS 13.0, *) -public class CfkFileProviderItem: NSObject, NSFileProviderItem { - - // MARK: - Properties - - private let cfkItem: CfkItemWrapper - - /// Unique identifier for this item - public var itemIdentifier: NSFileProviderItemIdentifier { - return NSFileProviderItemIdentifier(cfkItem.identifier) - } - - /// Parent folder identifier - public var parentItemIdentifier: NSFileProviderItemIdentifier { - if cfkItem.parentIdentifier == "root" { - return .rootContainer - } - return NSFileProviderItemIdentifier(cfkItem.parentIdentifier) - } - - /// File or folder name - public var filename: String { - return cfkItem.filename - } - - /// Content type (UTI) - public var contentType: UTType { - if cfkItem.itemType == CFK_ITEM_TYPE_DIRECTORY { - return .folder - } - // Infer from filename - if let type = UTType(filenameExtension: (filename as NSString).pathExtension) { - return type - } - return .data - } - - /// Document size - public var documentSize: NSNumber? { - guard cfkItem.hasSize else { return nil } - return NSNumber(value: cfkItem.size) - } - - /// Item capabilities - public var capabilities: NSFileProviderItemCapabilities { - var caps: NSFileProviderItemCapabilities = [] - - if cfkItem.capabilities & UInt64(CFK_CAP_READING) != 0 { - caps.insert(.allowsReading) - } - if cfkItem.capabilities & UInt64(CFK_CAP_WRITING) != 0 { - caps.insert(.allowsWriting) - } - if cfkItem.capabilities & UInt64(CFK_CAP_REPARENTING) != 0 { - caps.insert(.allowsReparenting) - } - if cfkItem.capabilities & UInt64(CFK_CAP_RENAMING) != 0 { - caps.insert(.allowsRenaming) - } - if cfkItem.capabilities & UInt64(CFK_CAP_TRASHING) != 0 { - caps.insert(.allowsTrashing) - } - if cfkItem.capabilities & UInt64(CFK_CAP_DELETING) != 0 { - caps.insert(.allowsDeleting) - } - if cfkItem.capabilities & UInt64(CFK_CAP_EVICTING) != 0 { - caps.insert(.allowsEvicting) - } - if cfkItem.capabilities & UInt64(CFK_CAP_ADDING_SUBITEM) != 0 { - caps.insert(.allowsAddingSubItems) - } - if cfkItem.capabilities & UInt64(CFK_CAP_CONTENT_ENUMERATION) != 0 { - caps.insert(.allowsContentEnumerating) - } - - return caps - } - - /// Whether content is downloaded - public var isDownloaded: Bool { - return cfkItem.isDownloaded - } - - /// Whether content is uploaded - public var isUploaded: Bool { - return cfkItem.isUploaded - } - - // MARK: - Initialization - - /// Initialize from CfkItem C structure - public init(from cItem: CfkItem) { - self.cfkItem = CfkItemWrapper(from: cItem) - super.init() - } - - /// Fetch item from Rust backend - public static func fetch(identifier: String) throws -> CfkFileProviderItem { - var cItem = CfkItem() - let result = identifier.withCString { ptr in - cfk_item_get(ptr, &cItem) - } - - guard result == CFK_ERROR_SUCCESS else { - throw CfkError.from(code: result) - } - - defer { cfk_item_free(&cItem) } - return CfkFileProviderItem(from: cItem) - } -} - -// MARK: - Swift Wrapper for C Structure - -/// Swift-friendly wrapper around CfkItem -private struct CfkItemWrapper { - let identifier: String - let parentIdentifier: String - let filename: String - let itemType: UInt32 - let size: UInt64 - let hasSize: Bool - let capabilities: UInt64 - let isDownloaded: Bool - let isUploaded: Bool - - init(from cItem: CfkItem) { - self.identifier = cItem.identifier.map { String(cString: $0) } ?? "" - self.parentIdentifier = cItem.parent_identifier.map { String(cString: $0) } ?? "root" - self.filename = cItem.filename.map { String(cString: $0) } ?? "" - self.itemType = cItem.item_type - self.size = cItem.size - self.hasSize = cItem.has_size - self.capabilities = cItem.capabilities - self.isDownloaded = cItem.is_downloaded - self.isUploaded = cItem.is_uploaded - } -} - -// MARK: - Error Handling - -/// CFK Error type -public enum CfkError: Error { - case noSuchItem - case itemAlreadyExists - case notAuthenticated - case serverUnreachable - case quotaExceeded - case filenameInvalid - case versionOutOfDate - case cannotSync - case unknown(Int32) - - static func from(code: Int32) -> CfkError { - switch code { - case CFK_ERROR_NO_SUCH_ITEM: - return .noSuchItem - case CFK_ERROR_ITEM_ALREADY_EXISTS: - return .itemAlreadyExists - case CFK_ERROR_NOT_AUTHENTICATED: - return .notAuthenticated - case CFK_ERROR_SERVER_UNREACHABLE: - return .serverUnreachable - case CFK_ERROR_QUOTA_EXCEEDED: - return .quotaExceeded - case CFK_ERROR_FILENAME_INVALID: - return .filenameInvalid - case CFK_ERROR_VERSION_OUT_OF_DATE: - return .versionOutOfDate - case CFK_ERROR_CANNOT_SYNC: - return .cannotSync - default: - return .unknown(code) - } - } - - /// Convert to NSFileProviderError - @available(iOS 16.0, macOS 13.0, *) - public var fileProviderError: NSError { - let code: NSFileProviderError.Code - switch self { - case .noSuchItem: - code = .noSuchItem - case .itemAlreadyExists: - code = .filenameCollision - case .notAuthenticated: - code = .notAuthenticated - case .serverUnreachable: - code = .serverUnreachable - case .quotaExceeded: - code = .insufficientQuota - case .filenameInvalid: - code = .filenameCollision - case .versionOutOfDate: - code = .newerExtensionVersionFound - case .cannotSync: - code = .cannotSynchronize - case .unknown: - code = .cannotSynchronize - } - return NSError(domain: NSFileProviderErrorDomain, code: code.rawValue) - } -} - -// MARK: - Initialization Helper - -/// Initialize CFK library -public func initializeCfk(storagePath: URL, cachePath: URL, tempPath: URL) throws { - // Initialize runtime - let initResult = cfk_ios_init() - guard initResult == 0 else { - throw CfkError.unknown(initResult) - } - - // Initialize provider - let result = storagePath.path.withCString { storage in - cachePath.path.withCString { cache in - tempPath.path.withCString { temp in - cfk_provider_init(storage, cache, temp) - } - } - } - - guard result == CFK_ERROR_SUCCESS else { - throw CfkError.from(code: result) - } -} - -/// Shutdown CFK library -public func shutdownCfk() { - cfk_ios_shutdown() -} diff --git a/czech-file-knife/cfk-providers/Cargo.toml b/czech-file-knife/cfk-providers/Cargo.toml deleted file mode 100644 index 967930ba6..000000000 --- a/czech-file-knife/cfk-providers/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "cfk-providers" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Storage backend providers for Czech File Knife" - -[features] -default = ["local"] -local = [] -dropbox = ["oauth2"] -gdrive = ["oauth2"] -onedrive = ["oauth2"] -box = ["oauth2"] -s3 = [] -ipfs = [] -webdav = [] -afs = [] -ninep = [] -sftp = [] -nfs = [] -smb = [] -syncthing = [] -all = ["local", "dropbox", "gdrive", "onedrive", "box", "s3", "ipfs", "webdav", "afs", "ninep", "sftp", "nfs", "smb", "syncthing"] - -[dependencies] -cfk-core = { path = "../cfk-core" } -async-trait.workspace = true -bytes.workspace = true -chrono.workspace = true -futures.workspace = true -reqwest = { workspace = true, optional = true } -oauth2 = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tokio.workspace = true -tracing.workspace = true -blake3.workspace = true -libc.workspace = true - -[dev-dependencies] -tempfile = "3.24" diff --git a/czech-file-knife/cfk-providers/src/afs.rs b/czech-file-knife/cfk-providers/src/afs.rs deleted file mode 100644 index c94eae4de..000000000 --- a/czech-file-knife/cfk-providers/src/afs.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! Andrew File System (AFS) storage backend -//! -//! OpenAFS client implementation with Kerberos authentication. -//! Designed for academic and enterprise distributed file systems. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use tokio::fs; - -/// AFS backend configuration -#[derive(Debug, Clone)] -pub struct AfsConfig { - /// AFS cell name (e.g., "athena.mit.edu") - pub cell: String, - /// Local AFS mount point (typically /afs) - pub mount_point: PathBuf, - /// Kerberos principal (optional, uses default if not set) - pub principal: Option, - /// Keytab file path (optional, for service accounts) - pub keytab: Option, -} - -impl Default for AfsConfig { - fn default() -> Self { - Self { - cell: String::new(), - mount_point: PathBuf::from("/afs"), - principal: None, - keytab: None, - } - } -} - -/// AFS storage backend -pub struct AfsBackend { - id: String, - config: AfsConfig, - capabilities: StorageCapabilities, -} - -impl AfsBackend { - pub fn new(id: impl Into, config: AfsConfig) -> Self { - Self { - id: id.into(), - config, - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: false, - versioning: false, - sharing: true, // ACLs - streaming: true, - resume: true, - watch: false, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - } - } - - /// Authenticate with Kerberos and obtain AFS tokens - pub fn authenticate(&self) -> CfkResult<()> { - // Use kinit for Kerberos authentication - if let Some(ref keytab) = self.config.keytab { - let principal = self - .config - .principal - .as_deref() - .ok_or_else(|| CfkError::Auth("Principal required with keytab".into()))?; - - let status = Command::new("kinit") - .args(["-k", "-t", keytab.to_str().unwrap(), principal]) - .status() - .map_err(|e| CfkError::Auth(format!("kinit failed: {}", e)))?; - - if !status.success() { - return Err(CfkError::Auth("kinit failed".into())); - } - } else if let Some(ref principal) = self.config.principal { - // Interactive kinit - let status = Command::new("kinit") - .arg(principal) - .status() - .map_err(|e| CfkError::Auth(format!("kinit failed: {}", e)))?; - - if !status.success() { - return Err(CfkError::Auth("kinit failed".into())); - } - } - - // Get AFS tokens using aklog - let status = Command::new("aklog") - .args(["-c", &self.config.cell]) - .status() - .map_err(|e| CfkError::Auth(format!("aklog failed: {}", e)))?; - - if !status.success() { - return Err(CfkError::Auth("aklog failed".into())); - } - - Ok(()) - } - - /// Check if we have valid AFS tokens - pub fn has_tokens(&self) -> bool { - Command::new("tokens") - .status() - .map(|s| s.success()) - .unwrap_or(false) - } - - /// Convert VirtualPath to local filesystem path - fn to_local_path(&self, path: &VirtualPath) -> PathBuf { - let mut local_path = self.config.mount_point.clone(); - local_path.push(&self.config.cell); - for segment in &path.segments { - local_path.push(segment); - } - local_path - } - - /// Convert local path to VirtualPath - fn to_virtual_path(&self, local_path: &Path) -> CfkResult { - let cell_path = self.config.mount_point.join(&self.config.cell); - let relative = local_path - .strip_prefix(&cell_path) - .map_err(|_| CfkError::InvalidPath("Path not in AFS cell".into()))?; - - let segments: Vec = relative - .components() - .filter_map(|c| c.as_os_str().to_str().map(String::from)) - .collect(); - - Ok(VirtualPath { - backend_id: self.id.clone(), - segments, - }) - } - - /// Get AFS ACL for a directory - pub fn get_acl(&self, path: &VirtualPath) -> CfkResult { - let local_path = self.to_local_path(path); - - let output = Command::new("fs") - .args(["listacl", local_path.to_str().unwrap()]) - .output() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !output.status.success() { - return Err(CfkError::ProviderApi { - provider: "afs".into(), - message: String::from_utf8_lossy(&output.stderr).to_string(), - }); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - parse_acl(&stdout) - } - - /// Set AFS ACL - pub fn set_acl(&self, path: &VirtualPath, principal: &str, rights: &str) -> CfkResult<()> { - let local_path = self.to_local_path(path); - - let status = Command::new("fs") - .args([ - "setacl", - local_path.to_str().unwrap(), - principal, - rights, - ]) - .status() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !status.success() { - return Err(CfkError::ProviderApi { - provider: "afs".into(), - message: "Failed to set ACL".into(), - }); - } - - Ok(()) - } - - /// Get quota information for a volume - pub fn get_quota(&self, path: &VirtualPath) -> CfkResult { - let local_path = self.to_local_path(path); - - let output = Command::new("fs") - .args(["listquota", local_path.to_str().unwrap()]) - .output() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !output.status.success() { - return Err(CfkError::ProviderApi { - provider: "afs".into(), - message: String::from_utf8_lossy(&output.stderr).to_string(), - }); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - parse_quota(&stdout) - } -} - -/// AFS Access Control List -#[derive(Debug, Clone, Default)] -pub struct AfsAcl { - pub positive: Vec, - pub negative: Vec, -} - -/// AFS ACL entry -#[derive(Debug, Clone)] -pub struct AfsAclEntry { - pub principal: String, - pub rights: String, -} - -/// AFS Quota information -#[derive(Debug, Clone, Default)] -pub struct AfsQuota { - pub volume: String, - pub quota_kb: u64, - pub used_kb: u64, - pub percent_used: f32, -} - -/// Parse AFS ACL output -fn parse_acl(output: &str) -> CfkResult { - let mut acl = AfsAcl::default(); - let mut in_positive = true; - - for line in output.lines() { - let line = line.trim(); - if line.starts_with("Access list for") || line.is_empty() { - continue; - } - if line.starts_with("Normal rights:") { - in_positive = true; - continue; - } - if line.starts_with("Negative rights:") { - in_positive = false; - continue; - } - - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 2 { - let entry = AfsAclEntry { - principal: parts[0].to_string(), - rights: parts[1].to_string(), - }; - if in_positive { - acl.positive.push(entry); - } else { - acl.negative.push(entry); - } - } - } - - Ok(acl) -} - -/// Parse AFS quota output -fn parse_quota(output: &str) -> CfkResult { - let mut quota = AfsQuota::default(); - - for line in output.lines().skip(1) { - // Skip header - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 4 { - quota.volume = parts[0].to_string(); - quota.quota_kb = parts[1].parse().unwrap_or(0); - quota.used_kb = parts[2].parse().unwrap_or(0); - quota.percent_used = parts[3] - .trim_end_matches('%') - .parse() - .unwrap_or(0.0); - break; - } - } - - Ok(quota) -} - -#[async_trait] -impl StorageBackend for AfsBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "AFS" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - let cell_path = self.config.mount_point.join(&self.config.cell); - cell_path.exists() && self.has_tokens() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let local_path = self.to_local_path(path); - - let metadata = fs::metadata(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - let kind = if metadata.is_dir() { - EntryKind::Directory - } else if metadata.is_symlink() { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let mut meta = Metadata::default(); - meta.size = Some(metadata.len()); - - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - meta.permissions = Some(metadata.mode()); - meta.uid = Some(metadata.uid()); - meta.gid = Some(metadata.gid()); - } - - if let Ok(modified) = metadata.modified() { - meta.modified = Some(chrono::DateTime::from(modified)); - } - if let Ok(created) = metadata.created() { - meta.created = Some(chrono::DateTime::from(created)); - } - - Ok(Entry { - path: path.clone(), - kind, - metadata: meta, - }) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let local_path = self.to_local_path(path); - - let mut entries = Vec::new(); - let mut dir = fs::read_dir(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - while let Some(entry) = dir - .next_entry() - .await - .map_err(|e| CfkError::Io(e.to_string()))? - { - let entry_path = entry.path(); - let virtual_path = self.to_virtual_path(&entry_path)?; - - let metadata = entry - .metadata() - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - let kind = if metadata.is_dir() { - EntryKind::Directory - } else if metadata.is_symlink() { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let mut meta = Metadata::default(); - meta.size = Some(metadata.len()); - - if let Ok(modified) = metadata.modified() { - meta.modified = Some(chrono::DateTime::from(modified)); - } - - entries.push(Entry { - path: virtual_path, - kind, - metadata: meta, - }); - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let local_path = self.to_local_path(path); - - let data = fs::read(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - Ok(Bytes::from(data)) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let local_path = self.to_local_path(path); - - // Ensure parent directory exists - if let Some(parent) = local_path.parent() { - fs::create_dir_all(parent) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - } - - fs::write(&local_path, &data) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - self.get_metadata(path).await - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let local_path = self.to_local_path(path); - - let metadata = fs::metadata(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - if metadata.is_dir() { - fs::remove_dir_all(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - } else { - fs::remove_file(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let local_path = self.to_local_path(path); - - fs::create_dir_all(&local_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - self.get_metadata(path).await - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_local_path(from); - let to_path = self.to_local_path(to); - - // Ensure parent directory exists - if let Some(parent) = to_path.parent() { - fs::create_dir_all(parent) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - } - - fs::copy(&from_path, &to_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - self.get_metadata(to).await - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_local_path(from); - let to_path = self.to_local_path(to); - - // Ensure parent directory exists - if let Some(parent) = to_path.parent() { - fs::create_dir_all(parent) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - } - - fs::rename(&from_path, &to_path) - .await - .map_err(|e| CfkError::Io(e.to_string()))?; - - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - let root = VirtualPath::new(&self.id, ""); - let quota = self.get_quota(&root)?; - - let total = quota.quota_kb * 1024; - let used = quota.used_kb * 1024; - let available = total.saturating_sub(used); - - Ok((available, total)) - } -} - -/// AFS rights constants -pub mod rights { - /// Read files - pub const READ: &str = "r"; - /// List directory - pub const LIST: &str = "l"; - /// Insert (create) files - pub const INSERT: &str = "i"; - /// Delete files - pub const DELETE: &str = "d"; - /// Write/modify files - pub const WRITE: &str = "w"; - /// Lock files - pub const LOCK: &str = "k"; - /// Administer ACLs - pub const ADMIN: &str = "a"; - - /// All rights - pub const ALL: &str = "rlidwka"; - /// Read-only - pub const READ_ONLY: &str = "rl"; - /// Write (no admin) - pub const WRITE_NO_ADMIN: &str = "rlidwk"; -} diff --git a/czech-file-knife/cfk-providers/src/box_com.rs b/czech-file-knife/cfk-providers/src/box_com.rs deleted file mode 100644 index d89eb54f7..000000000 --- a/czech-file-knife/cfk-providers/src/box_com.rs +++ /dev/null @@ -1,721 +0,0 @@ -//! Box.com storage backend -//! -//! Box API implementation with OAuth 2.0 authentication. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use oauth2::{ - basic::BasicClient, AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, - PkceCodeVerifier, RedirectUrl, Scope, TokenUrl, -}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; - -const BOX_AUTH_URL: &str = "https://account.box.com/api/oauth2/authorize"; -const BOX_TOKEN_URL: &str = "https://api.box.com/oauth2/token"; -const BOX_API_URL: &str = "https://api.box.com/2.0"; -const BOX_UPLOAD_URL: &str = "https://upload.box.com/api/2.0"; - -/// Box OAuth tokens -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BoxTokens { - pub access_token: String, - pub refresh_token: Option, - pub expires_at: Option>, -} - -/// Box backend configuration -#[derive(Debug, Clone)] -pub struct BoxConfig { - pub client_id: String, - pub client_secret: String, - pub redirect_uri: String, -} - -/// Box storage backend -pub struct BoxBackend { - id: String, - config: BoxConfig, - tokens: Arc>>, - http: Client, - capabilities: StorageCapabilities, - /// Cache of path to folder ID - folder_cache: Arc>>, -} - -impl BoxBackend { - pub fn new(id: impl Into, config: BoxConfig) -> Self { - Self { - id: id.into(), - config, - tokens: Arc::new(RwLock::new(None)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: true, - versioning: true, - sharing: true, - streaming: true, - resume: true, - watch: true, - metadata: true, - thumbnails: true, - max_file_size: Some(150 * 1024 * 1024 * 1024), // 150GB for enterprise - }, - folder_cache: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Start OAuth 2.0 flow - pub fn start_auth(&self) -> (String, PkceCodeVerifier) { - let client = BasicClient::new(ClientId::new(self.config.client_id.clone())) - .set_client_secret(ClientSecret::new(self.config.client_secret.clone())) - .set_auth_uri(AuthUrl::new(BOX_AUTH_URL.to_string()).unwrap()) - .set_token_uri(TokenUrl::new(BOX_TOKEN_URL.to_string()).unwrap()) - .set_redirect_uri(RedirectUrl::new(self.config.redirect_uri.clone()).unwrap()); - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, _csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("root_readwrite".to_string())) - .set_pkce_challenge(pkce_challenge) - .url(); - - (auth_url.to_string(), pkce_verifier) - } - - /// Complete OAuth flow - pub async fn complete_auth( - &self, - code: &str, - _verifier: PkceCodeVerifier, - ) -> CfkResult { - let params = [ - ("grant_type", "authorization_code"), - ("code", code), - ("client_id", &self.config.client_id), - ("client_secret", &self.config.client_secret), - ]; - - let response = self - .http - .post(BOX_TOKEN_URL) - .form(¶ms) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::Auth(format!("Token exchange failed: {}", error_text))); - } - - #[derive(Deserialize)] - struct TokenResponse { - access_token: String, - refresh_token: Option, - expires_in: Option, - } - - let token_resp: TokenResponse = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let tokens = BoxTokens { - access_token: token_resp.access_token, - refresh_token: token_resp.refresh_token, - expires_at: token_resp - .expires_in - .map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - }; - - *self.tokens.write().await = Some(tokens.clone()); - Ok(tokens) - } - - /// Set tokens directly - pub async fn set_tokens(&self, tokens: BoxTokens) { - *self.tokens.write().await = Some(tokens); - } - - /// Get access token - async fn get_access_token(&self) -> CfkResult { - let tokens = self.tokens.read().await; - tokens - .as_ref() - .map(|t| t.access_token.clone()) - .ok_or_else(|| CfkError::Auth("Not authenticated".into())) - } - - /// Resolve path to folder ID - async fn resolve_folder_id(&self, path: &VirtualPath) -> CfkResult { - if path.segments.is_empty() { - return Ok("0".to_string()); // Root folder - } - - let path_str = path.to_string(); - { - let cache = self.folder_cache.read().await; - if let Some(id) = cache.get(&path_str) { - return Ok(id.clone()); - } - } - - // Navigate path - let mut current_id = "0".to_string(); - - for segment in &path.segments { - let response = self - .http - .get(format!("{}/folders/{}/items", BOX_API_URL, current_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[("fields", "id,name,type")]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct ItemList { - entries: Vec, - } - - let list: ItemList = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - current_id = list - .entries - .iter() - .find(|e| e.name == *segment) - .map(|e| e.id.clone()) - .ok_or_else(|| CfkError::NotFound(path.to_string()))?; - } - - // Cache - { - let mut cache = self.folder_cache.write().await; - cache.insert(path_str, current_id.clone()); - } - - Ok(current_id) - } -} - -/// Box item metadata -#[derive(Debug, Clone, Deserialize)] -struct BoxItem { - id: String, - #[serde(rename = "type")] - item_type: String, - name: String, - size: Option, - created_at: Option, - modified_at: Option, - sha1: Option, -} - -impl BoxItem { - fn to_entry(&self, backend_id: &str, base_path: &str) -> Entry { - let path_str = if base_path.is_empty() { - self.name.clone() - } else { - format!("{}/{}", base_path, self.name) - }; - let virtual_path = VirtualPath::new(backend_id, &path_str); - - let kind = if self.item_type == "folder" { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = self.size; - - if let Some(ref modified) = self.modified_at { - if let Ok(dt) = DateTime::parse_from_rfc3339(modified) { - metadata.modified = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref created) = self.created_at { - if let Ok(dt) = DateTime::parse_from_rfc3339(created) { - metadata.created = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref sha1) = self.sha1 { - metadata.checksum = Some(sha1.clone()); - } - - Entry { - path: virtual_path, - kind, - metadata, - } - } -} - -#[async_trait] -impl StorageBackend for BoxBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Box" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.tokens.read().await.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let item_id = self.resolve_folder_id(path).await?; - - // Try as folder first - let response = self - .http - .get(format!("{}/folders/{}", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if response.status().is_success() { - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if path.segments.len() > 1 { - path.segments[..path.segments.len() - 1].join("/") - } else { - String::new() - }; - - return Ok(item.to_entry(&self.id, &base_path)); - } - - // Try as file - let response = self - .http - .get(format!("{}/files/{}", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if path.segments.len() > 1 { - path.segments[..path.segments.len() - 1].join("/") - } else { - String::new() - }; - - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let folder_id = self.resolve_folder_id(path).await?; - - let mut entries = Vec::new(); - let mut offset = 0; - let limit = 1000; - - loop { - let response = self - .http - .get(format!("{}/folders/{}/items", BOX_API_URL, folder_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[ - ("fields", "id,type,name,size,created_at,modified_at,sha1"), - ("limit", &limit.to_string()), - ("offset", &offset.to_string()), - ]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct ItemList { - entries: Vec, - total_count: u64, - } - - let list: ItemList = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = path.segments.join("/"); - - for item in &list.entries { - entries.push(item.to_entry(&self.id, &base_path)); - } - - offset += list.entries.len(); - if offset as u64 >= list.total_count { - break; - } - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let file_id = self.resolve_folder_id(path).await?; - - let response = self - .http - .get(format!("{}/files/{}/content", BOX_API_URL, file_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "box".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let parent_path = if path.segments.len() > 1 { - VirtualPath::new(&self.id, &path.segments[..path.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - - let parent_id = self.resolve_folder_id(&parent_path).await?; - let name = path.segments.last().cloned().unwrap_or_default(); - - // Use multipart upload - let boundary = "cfk_box_boundary"; - - #[derive(Serialize)] - struct FileAttributes { - name: String, - parent: Parent, - } - - #[derive(Serialize)] - struct Parent { - id: String, - } - - let attributes = FileAttributes { - name: name.clone(), - parent: Parent { id: parent_id }, - }; - - let attributes_json = - serde_json::to_string(&attributes).map_err(|e| CfkError::Serialization(e.to_string()))?; - - let body = format!( - "--{}\r\nContent-Disposition: form-data; name=\"attributes\"\r\n\r\n{}\r\n--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: application/octet-stream\r\n\r\n", - boundary, attributes_json, boundary, name - ); - - let mut full_body = body.into_bytes(); - full_body.extend_from_slice(&data); - full_body.extend_from_slice(format!("\r\n--{}--", boundary).as_bytes()); - - let response = self - .http - .post(format!("{}/files/content", BOX_UPLOAD_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .header( - "Content-Type", - format!("multipart/form-data; boundary={}", boundary), - ) - .body(full_body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct UploadResponse { - entries: Vec, - } - - let upload_resp: UploadResponse = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let item = upload_resp - .entries - .first() - .ok_or_else(|| CfkError::ProviderApi { - provider: "box".into(), - message: "No file returned".into(), - })?; - - let base_path = parent_path.segments.join("/"); - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let item_id = self.resolve_folder_id(path).await?; - - // Try as file first - let response = self - .http - .delete(format!("{}/files/{}", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if response.status().is_success() || response.status() == reqwest::StatusCode::NO_CONTENT { - return Ok(()); - } - - // Try as folder - let response = self - .http - .delete(format!("{}/folders/{}?recursive=true", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != reqwest::StatusCode::NO_CONTENT { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "box".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let parent_path = if path.segments.len() > 1 { - VirtualPath::new(&self.id, &path.segments[..path.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - - let parent_id = self.resolve_folder_id(&parent_path).await?; - let name = path.segments.last().cloned().unwrap_or_default(); - - #[derive(Serialize)] - struct CreateFolder { - name: String, - parent: Parent, - } - - #[derive(Serialize)] - struct Parent { - id: String, - } - - let body = CreateFolder { - name, - parent: Parent { id: parent_id }, - }; - - let response = self - .http - .post(format!("{}/folders", BOX_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = parent_path.segments.join("/"); - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let item_id = self.resolve_folder_id(from).await?; - let parent_path = if to.segments.len() > 1 { - VirtualPath::new(&self.id, &to.segments[..to.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - let parent_id = self.resolve_folder_id(&parent_path).await?; - let name = to.segments.last().cloned().unwrap_or_default(); - - #[derive(Serialize)] - struct CopyRequest { - parent: Parent, - name: String, - } - - #[derive(Serialize)] - struct Parent { - id: String, - } - - let body = CopyRequest { - parent: Parent { id: parent_id }, - name, - }; - - // Try as file - let response = self - .http - .post(format!("{}/files/{}/copy", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if response.status().is_success() { - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = parent_path.segments.join("/"); - return Ok(item.to_entry(&self.id, &base_path)); - } - - // Try as folder - let response = self - .http - .post(format!("{}/folders/{}/copy", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = parent_path.segments.join("/"); - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let item_id = self.resolve_folder_id(from).await?; - let name = to.segments.last().cloned().unwrap_or_default(); - - #[derive(Serialize)] - struct RenameRequest { - name: String, - } - - let body = RenameRequest { name }; - - // Try as file - let response = self - .http - .put(format!("{}/files/{}", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if response.status().is_success() { - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if to.segments.len() > 1 { - to.segments[..to.segments.len() - 1].join("/") - } else { - String::new() - }; - return Ok(item.to_entry(&self.id, &base_path)); - } - - // Try as folder - let response = self - .http - .put(format!("{}/folders/{}", BOX_API_URL, item_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: BoxItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if to.segments.len() > 1 { - to.segments[..to.segments.len() - 1].join("/") - } else { - String::new() - }; - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - let response = self - .http - .get(format!("{}/users/me", BOX_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[("fields", "space_amount,space_used")]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct User { - space_amount: Option, - space_used: Option, - } - - let user: User = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let total = user.space_amount.unwrap_or(0); - let used = user.space_used.unwrap_or(0); - let available = total.saturating_sub(used); - - Ok((available, total)) - } -} diff --git a/czech-file-knife/cfk-providers/src/ceph.rs b/czech-file-knife/cfk-providers/src/ceph.rs deleted file mode 100644 index 1e259c435..000000000 --- a/czech-file-knife/cfk-providers/src/ceph.rs +++ /dev/null @@ -1,454 +0,0 @@ -//! Ceph storage backend -//! -//! Distributed object storage via RADOS, CephFS, or S3/Swift gateway. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; - -/// Ceph access mode -#[derive(Debug, Clone)] -pub enum CephMode { - /// Direct RADOS object access - Rados { - monitors: Vec, - user: String, - key: String, - pool: String, - }, - /// CephFS filesystem access - CephFs { - monitors: Vec, - user: String, - key: String, - mount_path: String, - }, - /// Ceph Object Gateway (S3-compatible) - Rgw { - endpoint: String, - access_key: String, - secret_key: String, - bucket: String, - }, -} - -/// Ceph backend configuration -#[derive(Debug, Clone)] -pub struct CephConfig { - pub mode: CephMode, -} - -/// Ceph storage backend -/// -/// Note: This is a stub implementation. Full implementation would use -/// `ceph` or `rados` crate for RADOS, or the S3 backend for RGW. -pub struct CephBackend { - id: String, - config: CephConfig, - capabilities: StorageCapabilities, -} - -impl CephBackend { - pub fn new(id: impl Into, config: CephConfig) -> Self { - let caps = match &config.mode { - CephMode::Rados { .. } => StorageCapabilities { - read: true, - write: true, - delete: true, - rename: false, // RADOS doesn't have rename - copy: false, - list: true, - search: false, - versioning: false, - sharing: false, - streaming: true, - resume: true, // Offset reads/writes - watch: true, // RADOS watch/notify - metadata: true, - thumbnails: false, - max_file_size: None, - }, - CephMode::CephFs { .. } => StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: false, - versioning: false, // CephFS has snapshots - sharing: true, // POSIX ACLs - streaming: true, - resume: true, - watch: true, // inotify - metadata: true, - thumbnails: false, - max_file_size: None, - }, - CephMode::Rgw { .. } => StorageCapabilities { - read: true, - write: true, - delete: true, - rename: false, // S3-style - copy: true, - list: true, - search: false, - versioning: true, - sharing: true, // Presigned URLs - streaming: true, - resume: true, - watch: false, - metadata: true, - thumbnails: false, - max_file_size: Some(5 * 1024 * 1024 * 1024 * 1024), // 5TB - }, - }; - - Self { - id: id.into(), - config, - capabilities: caps, - } - } - - /// Create RADOS backend - pub fn rados( - id: impl Into, - monitors: Vec, - user: &str, - key: &str, - pool: &str, - ) -> Self { - Self::new( - id, - CephConfig { - mode: CephMode::Rados { - monitors, - user: user.to_string(), - key: key.to_string(), - pool: pool.to_string(), - }, - }, - ) - } - - /// Create CephFS backend - pub fn cephfs( - id: impl Into, - monitors: Vec, - user: &str, - key: &str, - mount_path: &str, - ) -> Self { - Self::new( - id, - CephConfig { - mode: CephMode::CephFs { - monitors, - user: user.to_string(), - key: key.to_string(), - mount_path: mount_path.to_string(), - }, - }, - ) - } - - /// Create RGW (S3) backend - pub fn rgw( - id: impl Into, - endpoint: &str, - access_key: &str, - secret_key: &str, - bucket: &str, - ) -> Self { - Self::new( - id, - CephConfig { - mode: CephMode::Rgw { - endpoint: endpoint.to_string(), - access_key: access_key.to_string(), - secret_key: secret_key.to_string(), - bucket: bucket.to_string(), - }, - }, - ) - } - - /// Connect to Ceph cluster - pub async fn connect(&self) -> CfkResult<()> { - match &self.config.mode { - CephMode::Rados { monitors, user, key, pool } => { - // Would use rados_create(), rados_conf_set(), rados_connect() - // rados_ioctx_create() for pool access - } - CephMode::CephFs { monitors, user, key, mount_path } => { - // Would use ceph_mount(), ceph_conf_set(), etc. - } - CephMode::Rgw { .. } => { - // Use S3 backend (already implemented) - return Ok(()); - } - } - - Err(CfkError::Unsupported( - "Ceph backend is a stub. Use rados/ceph crate or S3 backend for RGW.".into(), - )) - } - - /// Convert VirtualPath to object/path name - fn to_object_name(&self, path: &VirtualPath) -> String { - path.segments.join("/") - } -} - -#[async_trait] -impl StorageBackend for CephBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - match &self.config.mode { - CephMode::Rados { .. } => "Ceph RADOS", - CephMode::CephFs { .. } => "CephFS", - CephMode::Rgw { .. } => "Ceph RGW", - } - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - false // Would check cluster connection - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let _name = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_stat() for object - } - CephMode::CephFs { .. } => { - // Would use ceph_stat() - } - CephMode::Rgw { .. } => { - // Use S3 HEAD - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let _prefix = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_nobjects_list_open/next - } - CephMode::CephFs { .. } => { - // Would use ceph_readdir() - } - CephMode::Rgw { .. } => { - // Use S3 LIST - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let _name = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_read() - } - CephMode::CephFs { .. } => { - // Would use ceph_read() - } - CephMode::Rgw { .. } => { - // Use S3 GET - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn write_file(&self, path: &VirtualPath, _data: Bytes) -> CfkResult { - let _name = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_write_full() or rados_write() - } - CephMode::CephFs { .. } => { - // Would use ceph_write() - } - CephMode::Rgw { .. } => { - // Use S3 PUT - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let _name = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_remove() - } - CephMode::CephFs { .. } => { - // Would use ceph_unlink() - } - CephMode::Rgw { .. } => { - // Use S3 DELETE - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let _name = self.to_object_name(path); - - match &self.config.mode { - CephMode::Rados { .. } => { - // RADOS doesn't have directories - return Err(CfkError::Unsupported( - "RADOS doesn't support directories".into(), - )); - } - CephMode::CephFs { .. } => { - // Would use ceph_mkdir() - } - CephMode::Rgw { .. } => { - // Create zero-byte object with trailing / - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - match &self.config.mode { - CephMode::Rados { .. } => { - return Err(CfkError::Unsupported("RADOS doesn't support copy".into())); - } - CephMode::CephFs { .. } | CephMode::Rgw { .. } => { - // CephFS: read + write - // RGW: S3 COPY - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn rename(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - match &self.config.mode { - CephMode::Rados { .. } | CephMode::Rgw { .. } => { - return Err(CfkError::Unsupported( - "RADOS/RGW doesn't support rename".into(), - )); - } - CephMode::CephFs { .. } => { - // Would use ceph_rename() - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_cluster_stat() - } - CephMode::CephFs { .. } => { - // Would use ceph_statfs() - } - CephMode::Rgw { .. } => { - // RGW doesn't expose quota - return Ok((0, 0)); - } - } - - Err(CfkError::Unsupported("Ceph stub".into())) - } -} - -/// RADOS object extended attributes -impl CephBackend { - /// Get extended attribute - pub async fn getxattr(&self, _path: &VirtualPath, _name: &str) -> CfkResult> { - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_getxattr() - } - CephMode::CephFs { .. } => { - // Would use ceph_getxattr() - } - _ => {} - } - Err(CfkError::Unsupported("Ceph stub".into())) - } - - /// Set extended attribute - pub async fn setxattr(&self, _path: &VirtualPath, _name: &str, _value: &[u8]) -> CfkResult<()> { - match &self.config.mode { - CephMode::Rados { .. } => { - // Would use rados_setxattr() - } - CephMode::CephFs { .. } => { - // Would use ceph_setxattr() - } - _ => {} - } - Err(CfkError::Unsupported("Ceph stub".into())) - } - - /// Create snapshot (CephFS only) - pub async fn create_snapshot(&self, _path: &VirtualPath, _name: &str) -> CfkResult<()> { - match &self.config.mode { - CephMode::CephFs { .. } => { - // Would create .snap/name directory - } - _ => { - return Err(CfkError::Unsupported( - "Snapshots only supported on CephFS".into(), - )); - } - } - Err(CfkError::Unsupported("Ceph stub".into())) - } -} - -/// Ceph cluster statistics -#[derive(Debug, Clone, Default)] -pub struct ClusterStat { - pub kb: u64, - pub kb_used: u64, - pub kb_avail: u64, - pub num_objects: u64, -} - -/// Pool statistics -#[derive(Debug, Clone, Default)] -pub struct PoolStat { - pub num_bytes: u64, - pub num_kb: u64, - pub num_objects: u64, - pub num_object_clones: u64, - pub num_object_copies: u64, - pub num_rd: u64, - pub num_rd_kb: u64, - pub num_wr: u64, - pub num_wr_kb: u64, -} diff --git a/czech-file-knife/cfk-providers/src/dropbox.rs b/czech-file-knife/cfk-providers/src/dropbox.rs deleted file mode 100644 index dfd2ab032..000000000 --- a/czech-file-knife/cfk-providers/src/dropbox.rs +++ /dev/null @@ -1,582 +0,0 @@ -//! Dropbox storage backend -//! -//! Full implementation of Dropbox API v2 with OAuth 2.0 + PKCE authentication. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use oauth2::{ - basic::BasicClient, AuthUrl, ClientId, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, - RedirectUrl, Scope, TokenUrl, -}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tokio::sync::RwLock; - -const DROPBOX_AUTH_URL: &str = "https://www.dropbox.com/oauth2/authorize"; -const DROPBOX_TOKEN_URL: &str = "https://api.dropboxapi.com/oauth2/token"; -const DROPBOX_API_URL: &str = "https://api.dropboxapi.com/2"; -const DROPBOX_CONTENT_URL: &str = "https://content.dropboxapi.com/2"; - -/// Dropbox OAuth tokens -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DropboxTokens { - pub access_token: String, - pub refresh_token: Option, - pub expires_at: Option>, -} - -/// Dropbox backend configuration -#[derive(Debug, Clone)] -pub struct DropboxConfig { - pub client_id: String, - pub redirect_uri: String, -} - -/// Dropbox storage backend -pub struct DropboxBackend { - id: String, - config: DropboxConfig, - tokens: Arc>>, - http: Client, - capabilities: StorageCapabilities, -} - -impl DropboxBackend { - pub fn new(id: impl Into, config: DropboxConfig) -> Self { - Self { - id: id.into(), - config, - tokens: Arc::new(RwLock::new(None)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: true, - versioning: true, - sharing: true, - streaming: true, - resume: true, - watch: false, - metadata: true, - thumbnails: true, - max_file_size: Some(350 * 1024 * 1024 * 1024), // 350GB - }, - } - } - - /// Start OAuth 2.0 + PKCE flow - pub fn start_auth(&self) -> (String, PkceCodeVerifier) { - let client = BasicClient::new(ClientId::new(self.config.client_id.clone())) - .set_auth_uri(AuthUrl::new(DROPBOX_AUTH_URL.to_string()).unwrap()) - .set_token_uri(TokenUrl::new(DROPBOX_TOKEN_URL.to_string()).unwrap()) - .set_redirect_uri(RedirectUrl::new(self.config.redirect_uri.clone()).unwrap()); - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, _csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("files.metadata.read".to_string())) - .add_scope(Scope::new("files.metadata.write".to_string())) - .add_scope(Scope::new("files.content.read".to_string())) - .add_scope(Scope::new("files.content.write".to_string())) - .set_pkce_challenge(pkce_challenge) - .url(); - - (auth_url.to_string(), pkce_verifier) - } - - /// Complete OAuth flow with authorization code - pub async fn complete_auth( - &self, - code: &str, - verifier: PkceCodeVerifier, - ) -> CfkResult { - let params = [ - ("code", code), - ("grant_type", "authorization_code"), - ("client_id", &self.config.client_id), - ("redirect_uri", &self.config.redirect_uri), - ("code_verifier", verifier.secret()), - ]; - - let response = self - .http - .post(DROPBOX_TOKEN_URL) - .form(¶ms) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::Auth(format!("Token exchange failed: {}", error_text))); - } - - #[derive(Deserialize)] - struct TokenResponse { - access_token: String, - refresh_token: Option, - expires_in: Option, - } - - let token_resp: TokenResponse = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let tokens = DropboxTokens { - access_token: token_resp.access_token, - refresh_token: token_resp.refresh_token, - expires_at: token_resp - .expires_in - .map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - }; - - *self.tokens.write().await = Some(tokens.clone()); - Ok(tokens) - } - - /// Set tokens directly (for restoring from storage) - pub async fn set_tokens(&self, tokens: DropboxTokens) { - *self.tokens.write().await = Some(tokens); - } - - /// Get current access token - async fn get_access_token(&self) -> CfkResult { - let tokens = self.tokens.read().await; - tokens - .as_ref() - .map(|t| t.access_token.clone()) - .ok_or_else(|| CfkError::Auth("Not authenticated".into())) - } - - /// Make authenticated API request - async fn api_request Deserialize<'de>>( - &self, - endpoint: &str, - body: impl Serialize, - ) -> CfkResult { - let token = self.get_access_token().await?; - let url = format!("{}/{}", DROPBOX_API_URL, endpoint); - - let response = self - .http - .post(&url) - .header("Authorization", format!("Bearer {}", token)) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "dropbox".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string())) - } - - /// Convert Dropbox path to VirtualPath - fn to_virtual_path(&self, dropbox_path: &str) -> VirtualPath { - let path = dropbox_path.trim_start_matches('/'); - VirtualPath::new(&self.id, path) - } - - /// Convert VirtualPath to Dropbox path - fn to_dropbox_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - String::new() - } else { - format!("/{}", path.segments.join("/")) - } - } -} - -/// Dropbox file metadata response -#[derive(Debug, Deserialize)] -struct DropboxMetadata { - #[serde(rename = ".tag")] - tag: String, - name: String, - path_display: Option, - id: Option, - size: Option, - client_modified: Option, - server_modified: Option, - rev: Option, - content_hash: Option, -} - -impl DropboxMetadata { - fn to_entry(&self, backend_id: &str) -> Entry { - let path = self - .path_display - .as_deref() - .unwrap_or(&self.name) - .trim_start_matches('/'); - let virtual_path = VirtualPath::new(backend_id, path); - - let kind = if self.tag == "folder" { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = self.size; - if let Some(ref modified) = self.server_modified { - if let Ok(dt) = DateTime::parse_from_rfc3339(modified) { - metadata.modified = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref hash) = self.content_hash { - metadata.checksum = Some(hash.clone()); - } - - Entry { - path: virtual_path, - kind, - metadata, - } - } -} - -/// List folder response -#[derive(Debug, Deserialize)] -struct ListFolderResponse { - entries: Vec, - cursor: String, - has_more: bool, -} - -#[async_trait] -impl StorageBackend for DropboxBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Dropbox" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.tokens.read().await.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let dropbox_path = self.to_dropbox_path(path); - - if dropbox_path.is_empty() { - // Root folder - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - - #[derive(Serialize)] - struct GetMetadataArg { - path: String, - } - - let result: DropboxMetadata = self - .api_request("files/get_metadata", GetMetadataArg { path: dropbox_path }) - .await?; - - Ok(result.to_entry(&self.id)) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let dropbox_path = self.to_dropbox_path(path); - - #[derive(Serialize)] - struct ListFolderArg { - path: String, - recursive: bool, - include_deleted: bool, - limit: u32, - } - - let result: ListFolderResponse = self - .api_request( - "files/list_folder", - ListFolderArg { - path: if dropbox_path.is_empty() { - String::new() - } else { - dropbox_path - }, - recursive: false, - include_deleted: false, - limit: 2000, - }, - ) - .await?; - - let mut entries: Vec = result - .entries - .iter() - .map(|m| m.to_entry(&self.id)) - .collect(); - - // Handle pagination - let mut cursor = result.cursor; - let mut has_more = result.has_more; - - while has_more { - #[derive(Serialize)] - struct ListFolderContinueArg { - cursor: String, - } - - let continue_result: ListFolderResponse = self - .api_request( - "files/list_folder/continue", - ListFolderContinueArg { - cursor: cursor.clone(), - }, - ) - .await?; - - entries.extend(continue_result.entries.iter().map(|m| m.to_entry(&self.id))); - cursor = continue_result.cursor; - has_more = continue_result.has_more; - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let token = self.get_access_token().await?; - let dropbox_path = self.to_dropbox_path(path); - - #[derive(Serialize)] - struct DownloadArg { - path: String, - } - - let arg = serde_json::to_string(&DownloadArg { path: dropbox_path }) - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let response = self - .http - .post(format!("{}/files/download", DROPBOX_CONTENT_URL)) - .header("Authorization", format!("Bearer {}", token)) - .header("Dropbox-API-Arg", arg) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "dropbox".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let token = self.get_access_token().await?; - let dropbox_path = self.to_dropbox_path(path); - - #[derive(Serialize)] - struct UploadArg { - path: String, - mode: String, - autorename: bool, - mute: bool, - } - - let arg = serde_json::to_string(&UploadArg { - path: dropbox_path, - mode: "overwrite".to_string(), - autorename: false, - mute: false, - }) - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let response = self - .http - .post(format!("{}/files/upload", DROPBOX_CONTENT_URL)) - .header("Authorization", format!("Bearer {}", token)) - .header("Dropbox-API-Arg", arg) - .header("Content-Type", "application/octet-stream") - .body(data.to_vec()) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "dropbox".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let metadata: DropboxMetadata = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - Ok(metadata.to_entry(&self.id)) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let dropbox_path = self.to_dropbox_path(path); - - #[derive(Serialize)] - struct DeleteArg { - path: String, - } - - let _: serde_json::Value = self - .api_request("files/delete_v2", DeleteArg { path: dropbox_path }) - .await?; - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let dropbox_path = self.to_dropbox_path(path); - - #[derive(Serialize)] - struct CreateFolderArg { - path: String, - autorename: bool, - } - - #[derive(Deserialize)] - struct CreateFolderResult { - metadata: DropboxMetadata, - } - - let result: CreateFolderResult = self - .api_request( - "files/create_folder_v2", - CreateFolderArg { - path: dropbox_path, - autorename: false, - }, - ) - .await?; - - Ok(result.metadata.to_entry(&self.id)) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_dropbox_path(from); - let to_path = self.to_dropbox_path(to); - - #[derive(Serialize)] - struct CopyArg { - from_path: String, - to_path: String, - autorename: bool, - } - - #[derive(Deserialize)] - struct CopyResult { - metadata: DropboxMetadata, - } - - let result: CopyResult = self - .api_request( - "files/copy_v2", - CopyArg { - from_path, - to_path, - autorename: false, - }, - ) - .await?; - - Ok(result.metadata.to_entry(&self.id)) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_dropbox_path(from); - let to_path = self.to_dropbox_path(to); - - #[derive(Serialize)] - struct MoveArg { - from_path: String, - to_path: String, - autorename: bool, - } - - #[derive(Deserialize)] - struct MoveResult { - metadata: DropboxMetadata, - } - - let result: MoveResult = self - .api_request( - "files/move_v2", - MoveArg { - from_path, - to_path, - autorename: false, - }, - ) - .await?; - - Ok(result.metadata.to_entry(&self.id)) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - #[derive(Deserialize)] - struct SpaceUsage { - used: u64, - allocation: SpaceAllocation, - } - - #[derive(Deserialize)] - struct SpaceAllocation { - #[serde(rename = ".tag")] - tag: String, - allocated: Option, - } - - let result: SpaceUsage = self - .api_request("users/get_space_usage", serde_json::json!(null)) - .await?; - - let total = result.allocation.allocated.unwrap_or(0); - let used = result.used; - let available = total.saturating_sub(used); - - Ok((available, total)) - } -} diff --git a/czech-file-knife/cfk-providers/src/gdrive.rs b/czech-file-knife/cfk-providers/src/gdrive.rs deleted file mode 100644 index 2d872f5e9..000000000 --- a/czech-file-knife/cfk-providers/src/gdrive.rs +++ /dev/null @@ -1,731 +0,0 @@ -//! Google Drive storage backend -//! -//! Full implementation of Google Drive API v3 with OAuth 2.0 + PKCE authentication. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use oauth2::{ - basic::BasicClient, AuthUrl, ClientId, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, - RedirectUrl, Scope, TokenUrl, -}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; - -const GOOGLE_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; -const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token"; -const DRIVE_API_URL: &str = "https://www.googleapis.com/drive/v3"; -const DRIVE_UPLOAD_URL: &str = "https://www.googleapis.com/upload/drive/v3"; - -/// Google OAuth tokens -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GoogleTokens { - pub access_token: String, - pub refresh_token: Option, - pub expires_at: Option>, -} - -/// Google Drive backend configuration -#[derive(Debug, Clone)] -pub struct GoogleDriveConfig { - pub client_id: String, - pub client_secret: Option, - pub redirect_uri: String, -} - -/// Google Drive storage backend -pub struct GoogleDriveBackend { - id: String, - config: GoogleDriveConfig, - tokens: Arc>>, - http: Client, - capabilities: StorageCapabilities, - /// Cache of file ID to path mapping - path_cache: Arc>>, -} - -impl GoogleDriveBackend { - pub fn new(id: impl Into, config: GoogleDriveConfig) -> Self { - Self { - id: id.into(), - config, - tokens: Arc::new(RwLock::new(None)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: true, - versioning: true, - sharing: true, - streaming: true, - resume: true, - watch: true, - metadata: true, - thumbnails: true, - max_file_size: Some(5 * 1024 * 1024 * 1024 * 1024), // 5TB - }, - path_cache: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Start OAuth 2.0 + PKCE flow - pub fn start_auth(&self) -> (String, PkceCodeVerifier) { - let client = BasicClient::new(ClientId::new(self.config.client_id.clone())) - .set_auth_uri(AuthUrl::new(GOOGLE_AUTH_URL.to_string()).unwrap()) - .set_token_uri(TokenUrl::new(GOOGLE_TOKEN_URL.to_string()).unwrap()) - .set_redirect_uri(RedirectUrl::new(self.config.redirect_uri.clone()).unwrap()); - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, _csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new( - "https://www.googleapis.com/auth/drive".to_string(), - )) - .add_scope(Scope::new( - "https://www.googleapis.com/auth/drive.metadata.readonly".to_string(), - )) - .set_pkce_challenge(pkce_challenge) - .add_extra_param("access_type", "offline") - .add_extra_param("prompt", "consent") - .url(); - - (auth_url.to_string(), pkce_verifier) - } - - /// Complete OAuth flow with authorization code - pub async fn complete_auth( - &self, - code: &str, - verifier: PkceCodeVerifier, - ) -> CfkResult { - let mut params = vec![ - ("code", code.to_string()), - ("grant_type", "authorization_code".to_string()), - ("client_id", self.config.client_id.clone()), - ("redirect_uri", self.config.redirect_uri.clone()), - ("code_verifier", verifier.secret().to_string()), - ]; - - if let Some(ref secret) = self.config.client_secret { - params.push(("client_secret", secret.clone())); - } - - let response = self - .http - .post(GOOGLE_TOKEN_URL) - .form(¶ms) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::Auth(format!("Token exchange failed: {}", error_text))); - } - - #[derive(Deserialize)] - struct TokenResponse { - access_token: String, - refresh_token: Option, - expires_in: Option, - } - - let token_resp: TokenResponse = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let tokens = GoogleTokens { - access_token: token_resp.access_token, - refresh_token: token_resp.refresh_token, - expires_at: token_resp - .expires_in - .map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - }; - - *self.tokens.write().await = Some(tokens.clone()); - Ok(tokens) - } - - /// Set tokens directly - pub async fn set_tokens(&self, tokens: GoogleTokens) { - *self.tokens.write().await = Some(tokens); - } - - /// Get current access token - async fn get_access_token(&self) -> CfkResult { - let tokens = self.tokens.read().await; - tokens - .as_ref() - .map(|t| t.access_token.clone()) - .ok_or_else(|| CfkError::Auth("Not authenticated".into())) - } - - /// Resolve path to file ID - async fn resolve_file_id(&self, path: &VirtualPath) -> CfkResult { - if path.segments.is_empty() { - return Ok("root".to_string()); - } - - // Check cache first - let path_str = path.to_string(); - { - let cache = self.path_cache.read().await; - if let Some(id) = cache.get(&path_str) { - return Ok(id.clone()); - } - } - - // Resolve path segment by segment - let mut current_id = "root".to_string(); - - for segment in &path.segments { - let query = format!( - "'{}' in parents and name = '{}' and trashed = false", - current_id, segment - ); - - let response = self - .http - .get(format!("{}/files", DRIVE_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[("q", &query), ("fields", &"files(id,name)".to_string())]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct FileList { - files: Vec, - } - - let list: FileList = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - current_id = list - .files - .first() - .map(|f| f.id.clone()) - .ok_or_else(|| CfkError::NotFound(path.to_string()))?; - } - - // Cache the result - { - let mut cache = self.path_cache.write().await; - cache.insert(path_str, current_id.clone()); - } - - Ok(current_id) - } - - /// Convert VirtualPath to parent folder ID and filename - fn path_to_parent_and_name(&self, path: &VirtualPath) -> (String, String) { - if path.segments.is_empty() { - ("root".to_string(), String::new()) - } else if path.segments.len() == 1 { - ("root".to_string(), path.segments[0].clone()) - } else { - let parent_segments = &path.segments[..path.segments.len() - 1]; - let parent_path = parent_segments.join("/"); - let name = path.segments.last().unwrap().clone(); - (parent_path, name) - } - } -} - -/// Google Drive file metadata -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DriveFile { - id: String, - name: String, - mime_type: String, - #[serde(default)] - size: Option, - created_time: Option, - modified_time: Option, - #[serde(default)] - parents: Vec, - #[serde(default)] - trashed: bool, - md5_checksum: Option, -} - -impl DriveFile { - fn to_entry(&self, backend_id: &str, path: &str) -> Entry { - let virtual_path = VirtualPath::new(backend_id, path); - - let kind = if self.mime_type == "application/vnd.google-apps.folder" { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = self.size.as_ref().and_then(|s| s.parse().ok()); - metadata.mime_type = Some(self.mime_type.clone()); - - if let Some(ref modified) = self.modified_time { - if let Ok(dt) = DateTime::parse_from_rfc3339(modified) { - metadata.modified = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref created) = self.created_time { - if let Ok(dt) = DateTime::parse_from_rfc3339(created) { - metadata.created = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref checksum) = self.md5_checksum { - metadata.checksum = Some(checksum.clone()); - } - - Entry { - path: virtual_path, - kind, - metadata, - } - } -} - -#[async_trait] -impl StorageBackend for GoogleDriveBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Google Drive" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.tokens.read().await.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let file_id = self.resolve_file_id(path).await?; - - let response = self - .http - .get(format!("{}/files/{}", DRIVE_API_URL, file_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[( - "fields", - "id,name,mimeType,size,createdTime,modifiedTime,parents,trashed,md5Checksum", - )]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - if status == reqwest::StatusCode::NOT_FOUND { - return Err(CfkError::NotFound(path.to_string())); - } - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "gdrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let file: DriveFile = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let path_str = path.segments.join("/"); - Ok(file.to_entry(&self.id, &path_str)) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let folder_id = self.resolve_file_id(path).await?; - - let mut entries = Vec::new(); - let mut page_token: Option = None; - - loop { - let query = format!("'{}' in parents and trashed = false", folder_id); - - let mut request = self - .http - .get(format!("{}/files", DRIVE_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[ - ("q", query.as_str()), - ( - "fields", - "nextPageToken,files(id,name,mimeType,size,createdTime,modifiedTime,md5Checksum)", - ), - ("pageSize", "1000"), - ]); - - if let Some(ref token) = page_token { - request = request.query(&[("pageToken", token.as_str())]); - } - - let response = request - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct FileList { - files: Vec, - next_page_token: Option, - } - - let list: FileList = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if path.segments.is_empty() { - String::new() - } else { - format!("{}/", path.segments.join("/")) - }; - - for file in list.files { - let file_path = format!("{}{}", base_path, file.name); - entries.push(file.to_entry(&self.id, &file_path)); - } - - page_token = list.next_page_token; - if page_token.is_none() { - break; - } - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let file_id = self.resolve_file_id(path).await?; - - let response = self - .http - .get(format!("{}/files/{}?alt=media", DRIVE_API_URL, file_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "gdrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let token = self.get_access_token().await?; - - // Check if file exists - let existing_id = self.resolve_file_id(path).await.ok(); - - let (parent_path, name) = self.path_to_parent_and_name(path); - let parent_id = if parent_path == "root" { - "root".to_string() - } else { - let parent_virtual = VirtualPath::new(&self.id, &parent_path); - self.resolve_file_id(&parent_virtual).await? - }; - - let file: DriveFile = if let Some(file_id) = existing_id { - // Update existing file - let response = self - .http - .patch(format!( - "{}/files/{}?uploadType=media", - DRIVE_UPLOAD_URL, file_id - )) - .header("Authorization", format!("Bearer {}", token)) - .header("Content-Type", "application/octet-stream") - .body(data.to_vec()) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))? - } else { - // Create new file - #[derive(Serialize)] - struct FileMetadata { - name: String, - parents: Vec, - } - - let metadata = FileMetadata { - name: name.clone(), - parents: vec![parent_id], - }; - - let metadata_json = - serde_json::to_string(&metadata).map_err(|e| CfkError::Serialization(e.to_string()))?; - - // Use multipart upload - let boundary = "cfk_boundary_12345"; - let body = format!( - "--{}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n{}\r\n--{}\r\nContent-Type: application/octet-stream\r\n\r\n", - boundary, metadata_json, boundary - ); - - let mut full_body = body.into_bytes(); - full_body.extend_from_slice(&data); - full_body.extend_from_slice(format!("\r\n--{}--", boundary).as_bytes()); - - let response = self - .http - .post(format!("{}?uploadType=multipart", DRIVE_UPLOAD_URL)) - .header("Authorization", format!("Bearer {}", token)) - .header( - "Content-Type", - format!("multipart/related; boundary={}", boundary), - ) - .body(full_body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))? - }; - - let path_str = path.segments.join("/"); - Ok(file.to_entry(&self.id, &path_str)) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let file_id = self.resolve_file_id(path).await?; - - let response = self - .http - .delete(format!("{}/files/{}", DRIVE_API_URL, file_id)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != reqwest::StatusCode::NO_CONTENT { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "gdrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - // Invalidate cache - { - let mut cache = self.path_cache.write().await; - cache.remove(&path.to_string()); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let token = self.get_access_token().await?; - let (parent_path, name) = self.path_to_parent_and_name(path); - - let parent_id = if parent_path == "root" { - "root".to_string() - } else { - let parent_virtual = VirtualPath::new(&self.id, &parent_path); - self.resolve_file_id(&parent_virtual).await? - }; - - #[derive(Serialize)] - #[serde(rename_all = "camelCase")] - struct FolderMetadata { - name: String, - mime_type: String, - parents: Vec, - } - - let metadata = FolderMetadata { - name, - mime_type: "application/vnd.google-apps.folder".to_string(), - parents: vec![parent_id], - }; - - let response = self - .http - .post(format!("{}/files", DRIVE_API_URL)) - .header("Authorization", format!("Bearer {}", token)) - .json(&metadata) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let file: DriveFile = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let path_str = path.segments.join("/"); - Ok(file.to_entry(&self.id, &path_str)) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let token = self.get_access_token().await?; - let file_id = self.resolve_file_id(from).await?; - - let (parent_path, name) = self.path_to_parent_and_name(to); - let parent_id = if parent_path == "root" { - "root".to_string() - } else { - let parent_virtual = VirtualPath::new(&self.id, &parent_path); - self.resolve_file_id(&parent_virtual).await? - }; - - #[derive(Serialize)] - struct CopyMetadata { - name: String, - parents: Vec, - } - - let metadata = CopyMetadata { - name, - parents: vec![parent_id], - }; - - let response = self - .http - .post(format!("{}/files/{}/copy", DRIVE_API_URL, file_id)) - .header("Authorization", format!("Bearer {}", token)) - .json(&metadata) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let file: DriveFile = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let path_str = to.segments.join("/"); - Ok(file.to_entry(&self.id, &path_str)) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let token = self.get_access_token().await?; - let file_id = self.resolve_file_id(from).await?; - - let (parent_path, name) = self.path_to_parent_and_name(to); - let parent_id = if parent_path == "root" { - "root".to_string() - } else { - let parent_virtual = VirtualPath::new(&self.id, &parent_path); - self.resolve_file_id(&parent_virtual).await? - }; - - #[derive(Serialize)] - struct UpdateMetadata { - name: String, - } - - let metadata = UpdateMetadata { name }; - - let response = self - .http - .patch(format!( - "{}/files/{}?addParents={}&removeParents={}", - DRIVE_API_URL, file_id, parent_id, "root" - )) - .header("Authorization", format!("Bearer {}", token)) - .json(&metadata) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let file: DriveFile = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - // Invalidate cache - { - let mut cache = self.path_cache.write().await; - cache.remove(&from.to_string()); - } - - let path_str = to.segments.join("/"); - Ok(file.to_entry(&self.id, &path_str)) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - let response = self - .http - .get(format!("{}/about", DRIVE_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .query(&[("fields", "storageQuota")]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct About { - storage_quota: StorageQuota, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct StorageQuota { - limit: Option, - usage: Option, - } - - let about: About = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let total: u64 = about - .storage_quota - .limit - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - let used: u64 = about - .storage_quota - .usage - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - let available = total.saturating_sub(used); - - Ok((available, total)) - } -} diff --git a/czech-file-knife/cfk-providers/src/ipfs.rs b/czech-file-knife/cfk-providers/src/ipfs.rs deleted file mode 100644 index bb527a17c..000000000 --- a/czech-file-knife/cfk-providers/src/ipfs.rs +++ /dev/null @@ -1,802 +0,0 @@ -//! IPFS storage backend -//! -//! Content-addressed distributed file system. -//! Supports local IPFS daemon, pinning services, and MFS (Mutable File System). - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use reqwest::{multipart, Client}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tokio::sync::RwLock; - -const DEFAULT_API_URL: &str = "http://127.0.0.1:5001/api/v0"; -const DEFAULT_GATEWAY_URL: &str = "http://127.0.0.1:8080"; - -/// IPFS backend configuration -#[derive(Debug, Clone)] -pub struct IpfsConfig { - /// IPFS API URL (default: http://127.0.0.1:5001/api/v0) - pub api_url: String, - /// IPFS Gateway URL (default: http://127.0.0.1:8080) - pub gateway_url: String, - /// Use MFS (Mutable File System) for path-based operations - pub use_mfs: bool, - /// Pin files after adding - pub auto_pin: bool, -} - -impl Default for IpfsConfig { - fn default() -> Self { - Self { - api_url: DEFAULT_API_URL.to_string(), - gateway_url: DEFAULT_GATEWAY_URL.to_string(), - use_mfs: true, - auto_pin: true, - } - } -} - -/// IPFS storage backend -pub struct IpfsBackend { - id: String, - config: Arc>, - http: Client, - capabilities: StorageCapabilities, -} - -impl IpfsBackend { - pub fn new(id: impl Into, config: IpfsConfig) -> Self { - Self { - id: id.into(), - config: Arc::new(RwLock::new(config)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: false, - versioning: true, // Content-addressed = immutable versions - sharing: true, - streaming: true, - resume: false, - watch: false, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - } - } - - /// Make API POST request - async fn api_post(&self, endpoint: &str, params: &[(&str, &str)]) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/{}", config.api_url, endpoint); - - let mut request = self.http.post(&url); - - for (key, value) in params { - request = request.query(&[(key, value)]); - } - - let response = request - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "ipfs".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .text() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - /// Make API POST request with JSON response - async fn api_post_json Deserialize<'de>>( - &self, - endpoint: &str, - params: &[(&str, &str)], - ) -> CfkResult { - let text = self.api_post(endpoint, params).await?; - serde_json::from_str(&text).map_err(|e| CfkError::Serialization(e.to_string())) - } - - /// Add content to IPFS - pub async fn add(&self, data: Bytes, name: Option<&str>) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/add", config.api_url); - - let mut params = vec![("pin", if config.auto_pin { "true" } else { "false" })]; - if let Some(n) = name { - params.push(("path", n)); - } - - let part = multipart::Part::bytes(data.to_vec()) - .file_name(name.unwrap_or("file").to_string()); - let form = multipart::Form::new().part("file", part); - - let mut request = self.http.post(&url); - for (key, value) in ¶ms { - request = request.query(&[(key, value)]); - } - - let response = request - .multipart(form) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "ipfs".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let text = response - .text() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - // IPFS returns newline-delimited JSON for directories - let last_line = text.lines().last().unwrap_or(&text); - serde_json::from_str(last_line).map_err(|e| CfkError::Serialization(e.to_string())) - } - - /// Get content by CID - pub async fn cat(&self, cid: &str) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/cat", config.api_url); - - let response = self - .http - .post(&url) - .query(&[("arg", cid)]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "ipfs".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - /// Pin a CID - pub async fn pin(&self, cid: &str) -> CfkResult<()> { - self.api_post("pin/add", &[("arg", cid)]).await?; - Ok(()) - } - - /// Unpin a CID - pub async fn unpin(&self, cid: &str) -> CfkResult<()> { - self.api_post("pin/rm", &[("arg", cid)]).await?; - Ok(()) - } - - /// List pinned CIDs - pub async fn list_pins(&self) -> CfkResult> { - #[derive(Deserialize)] - struct PinLsResponse { - #[serde(rename = "Keys")] - keys: std::collections::HashMap, - } - - #[derive(Deserialize)] - struct PinType { - #[serde(rename = "Type")] - pin_type: String, - } - - let resp: PinLsResponse = self.api_post_json("pin/ls", &[("type", "all")]).await?; - - Ok(resp - .keys - .into_iter() - .map(|(cid, pt)| PinInfo { - cid, - pin_type: pt.pin_type, - }) - .collect()) - } - - /// MFS: Write file to path - async fn mfs_write(&self, path: &str, data: Bytes) -> CfkResult<()> { - let config = self.config.read().await; - let url = format!("{}/files/write", config.api_url); - - let part = multipart::Part::bytes(data.to_vec()).file_name("file"); - let form = multipart::Form::new().part("file", part); - - let response = self - .http - .post(&url) - .query(&[ - ("arg", path), - ("create", "true"), - ("parents", "true"), - ("truncate", "true"), - ]) - .multipart(form) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "ipfs".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(()) - } - - /// MFS: Read file from path - async fn mfs_read(&self, path: &str) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/files/read", config.api_url); - - let response = self - .http - .post(&url) - .query(&[("arg", path)]) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "ipfs".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - /// MFS: List directory - async fn mfs_ls(&self, path: &str) -> CfkResult> { - #[derive(Deserialize)] - struct LsResponse { - #[serde(rename = "Entries")] - entries: Option>, - } - - let resp: LsResponse = self - .api_post_json("files/ls", &[("arg", path), ("long", "true")]) - .await?; - - Ok(resp.entries.unwrap_or_default()) - } - - /// MFS: Get file/directory stat - async fn mfs_stat(&self, path: &str) -> CfkResult { - self.api_post_json("files/stat", &[("arg", path)]).await - } - - /// MFS: Create directory - async fn mfs_mkdir(&self, path: &str) -> CfkResult<()> { - self.api_post("files/mkdir", &[("arg", path), ("parents", "true")]) - .await?; - Ok(()) - } - - /// MFS: Remove file/directory - async fn mfs_rm(&self, path: &str, recursive: bool) -> CfkResult<()> { - self.api_post( - "files/rm", - &[("arg", path), ("recursive", if recursive { "true" } else { "false" })], - ) - .await?; - Ok(()) - } - - /// MFS: Copy - async fn mfs_cp(&self, from: &str, to: &str) -> CfkResult<()> { - self.api_post("files/cp", &[("arg", from), ("arg", to)]) - .await?; - Ok(()) - } - - /// MFS: Move - async fn mfs_mv(&self, from: &str, to: &str) -> CfkResult<()> { - self.api_post("files/mv", &[("arg", from), ("arg", to)]) - .await?; - Ok(()) - } - - /// Convert VirtualPath to MFS path - fn to_mfs_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - "/".to_string() - } else { - format!("/{}", path.segments.join("/")) - } - } -} - -/// IPFS add response -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AddResponse { - #[serde(rename = "Name")] - pub name: String, - #[serde(rename = "Hash")] - pub hash: String, - #[serde(rename = "Size")] - pub size: String, -} - -/// Pin information -#[derive(Debug, Clone)] -pub struct PinInfo { - pub cid: String, - pub pin_type: String, -} - -/// MFS directory entry -#[derive(Debug, Clone, Deserialize)] -pub struct MfsEntry { - #[serde(rename = "Name")] - pub name: String, - #[serde(rename = "Type")] - pub entry_type: u8, - #[serde(rename = "Size")] - pub size: u64, - #[serde(rename = "Hash")] - pub hash: String, -} - -/// MFS stat response -#[derive(Debug, Clone, Deserialize)] -pub struct MfsStat { - #[serde(rename = "Hash")] - pub hash: String, - #[serde(rename = "Size")] - pub size: u64, - #[serde(rename = "CumulativeSize")] - pub cumulative_size: u64, - #[serde(rename = "Type")] - pub entry_type: String, -} - -#[async_trait] -impl StorageBackend for IpfsBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "IPFS" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - #[derive(Deserialize)] - struct IdResponse { - #[serde(rename = "ID")] - _id: String, - } - - self.api_post_json::("id", &[]).await.is_ok() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - let stat = self.mfs_stat(&mfs_path).await?; - - let kind = if stat.entry_type == "directory" { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(stat.size); - metadata.checksum = Some(stat.hash); - - return Ok(Entry { - path: path.clone(), - kind, - metadata, - }); - } - - // For CID-based paths - if path.segments.is_empty() { - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - - // Assume first segment is CID - let cid = &path.segments[0]; - - #[derive(Deserialize)] - struct ObjectStat { - #[serde(rename = "Hash")] - hash: String, - #[serde(rename = "NumLinks")] - num_links: u64, - #[serde(rename = "DataSize")] - data_size: u64, - #[serde(rename = "CumulativeSize")] - cumulative_size: u64, - } - - let stat: ObjectStat = self.api_post_json("object/stat", &[("arg", cid)]).await?; - - let kind = if stat.num_links > 0 { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(stat.cumulative_size); - metadata.checksum = Some(stat.hash); - - Ok(Entry { - path: path.clone(), - kind, - metadata, - }) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - let entries = self.mfs_ls(&mfs_path).await?; - - let base_path = if path.segments.is_empty() { - String::new() - } else { - format!("{}/", path.segments.join("/")) - }; - - return Ok(entries - .iter() - .map(|e| { - let kind = if e.entry_type == 1 { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(e.size); - metadata.checksum = Some(e.hash.clone()); - - Entry { - path: VirtualPath::new(&self.id, &format!("{}{}", base_path, e.name)), - kind, - metadata, - } - }) - .collect()); - } - - // For non-MFS, list pins at root - if path.segments.is_empty() { - let pins = self.list_pins().await?; - - return Ok(pins - .iter() - .map(|p| { - let mut metadata = Metadata::default(); - metadata.custom.insert("pin_type".to_string(), p.pin_type.clone()); - - Entry { - path: VirtualPath::new(&self.id, &p.cid), - kind: EntryKind::File, // Assume file, could be directory - metadata, - } - }) - .collect()); - } - - // List IPFS directory by CID - let cid = &path.segments[0]; - - #[derive(Deserialize)] - struct LsResponse { - #[serde(rename = "Objects")] - objects: Vec, - } - - #[derive(Deserialize)] - struct LsObject { - #[serde(rename = "Links")] - links: Vec, - } - - #[derive(Deserialize)] - struct LsLink { - #[serde(rename = "Name")] - name: String, - #[serde(rename = "Hash")] - hash: String, - #[serde(rename = "Size")] - size: u64, - #[serde(rename = "Type")] - link_type: u64, - } - - let resp: LsResponse = self.api_post_json("ls", &[("arg", cid)]).await?; - - let links = resp - .objects - .first() - .map(|o| &o.links) - .cloned() - .unwrap_or_default(); - - let base_path = path.segments.join("/"); - - Ok(links - .iter() - .map(|l| { - let kind = if l.link_type == 1 { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(l.size); - metadata.checksum = Some(l.hash.clone()); - - Entry { - path: VirtualPath::new(&self.id, &format!("{}/{}", base_path, l.name)), - kind, - metadata, - } - }) - .collect()) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - return self.mfs_read(&mfs_path).await; - } - - // Read by CID - if path.segments.is_empty() { - return Err(CfkError::InvalidPath("No CID specified".into())); - } - - let cid = path.segments.join("/"); - self.cat(&cid).await - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - self.mfs_write(&mfs_path, data).await?; - - // Get updated metadata - let stat = self.mfs_stat(&mfs_path).await?; - - let mut metadata = Metadata::default(); - metadata.size = Some(stat.size); - metadata.checksum = Some(stat.hash); - - return Ok(Entry { - path: path.clone(), - kind: EntryKind::File, - metadata, - }); - } - - // Add to IPFS and return the CID path - let name = path.segments.last().map(String::as_str); - let add_resp = self.add(data, name).await?; - - let mut metadata = Metadata::default(); - metadata.size = Some(add_resp.size.parse().unwrap_or(0)); - metadata.checksum = Some(add_resp.hash.clone()); - - Ok(Entry { - path: VirtualPath::new(&self.id, &add_resp.hash), - kind: EntryKind::File, - metadata, - }) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - return self.mfs_rm(&mfs_path, true).await; - } - - // For CID paths, unpin - if !path.segments.is_empty() { - let cid = &path.segments[0]; - return self.unpin(cid).await; - } - - Err(CfkError::InvalidPath("Cannot delete root".into())) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let mfs_path = self.to_mfs_path(path); - self.mfs_mkdir(&mfs_path).await?; - - let stat = self.mfs_stat(&mfs_path).await?; - - let mut metadata = Metadata::default(); - metadata.checksum = Some(stat.hash); - - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata, - }); - } - - Err(CfkError::Unsupported( - "Cannot create directory without MFS enabled".into(), - )) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let from_path = self.to_mfs_path(from); - let to_path = self.to_mfs_path(to); - self.mfs_cp(&from_path, &to_path).await?; - - return self.get_metadata(to).await; - } - - Err(CfkError::Unsupported("Copy requires MFS enabled".into())) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let config = self.config.read().await; - - if config.use_mfs { - let from_path = self.to_mfs_path(from); - let to_path = self.to_mfs_path(to); - self.mfs_mv(&from_path, &to_path).await?; - - return self.get_metadata(to).await; - } - - Err(CfkError::Unsupported("Rename requires MFS enabled".into())) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - #[derive(Deserialize)] - struct RepoStat { - #[serde(rename = "RepoSize")] - repo_size: u64, - #[serde(rename = "StorageMax")] - storage_max: u64, - } - - let stat: RepoStat = self.api_post_json("repo/stat", &[]).await?; - - let available = stat.storage_max.saturating_sub(stat.repo_size); - Ok((available, stat.storage_max)) - } -} - -/// Additional IPFS-specific operations -impl IpfsBackend { - /// Resolve IPNS name to CID - pub async fn resolve_ipns(&self, name: &str) -> CfkResult { - #[derive(Deserialize)] - struct ResolveResponse { - #[serde(rename = "Path")] - path: String, - } - - let resp: ResolveResponse = self - .api_post_json("name/resolve", &[("arg", name)]) - .await?; - - Ok(resp.path) - } - - /// Publish to IPNS - pub async fn publish_ipns(&self, cid: &str) -> CfkResult { - #[derive(Deserialize)] - struct PublishResponse { - #[serde(rename = "Name")] - name: String, - #[serde(rename = "Value")] - value: String, - } - - let resp: PublishResponse = self - .api_post_json("name/publish", &[("arg", cid)]) - .await?; - - Ok(resp.name) - } - - /// Get peer information - pub async fn swarm_peers(&self) -> CfkResult> { - #[derive(Deserialize)] - struct SwarmPeersResponse { - #[serde(rename = "Peers")] - peers: Option>, - } - - #[derive(Deserialize)] - struct PeerInfo { - #[serde(rename = "Peer")] - peer: String, - } - - let resp: SwarmPeersResponse = self.api_post_json("swarm/peers", &[]).await?; - - Ok(resp - .peers - .unwrap_or_default() - .into_iter() - .map(|p| p.peer) - .collect()) - } - - /// Garbage collect unpinned blocks - pub async fn repo_gc(&self) -> CfkResult<()> { - self.api_post("repo/gc", &[]).await?; - Ok(()) - } -} diff --git a/czech-file-knife/cfk-providers/src/lib.rs b/czech-file-knife/cfk-providers/src/lib.rs deleted file mode 100644 index 7cb254246..000000000 --- a/czech-file-knife/cfk-providers/src/lib.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Storage providers for Czech File Knife -//! -//! Supports 15+ backends: local, cloud, distributed, and exotic filesystems. -//! Plus exotic protocols: Gopher, Gemini, NNTP, RTSP, BitTorrent, etc. -//! Transport layers: TCP, QUIC, UDP, Unix sockets. - -mod local; -pub mod protocols; -pub mod transport; - -#[cfg(feature = "dropbox")] -pub mod dropbox; - -#[cfg(feature = "gdrive")] -pub mod gdrive; - -#[cfg(feature = "onedrive")] -pub mod onedrive; - -#[cfg(feature = "box")] -pub mod box_com; - -#[cfg(feature = "s3")] -pub mod s3; - -#[cfg(feature = "ipfs")] -pub mod ipfs; - -#[cfg(feature = "webdav")] -pub mod webdav; - -#[cfg(feature = "afs")] -pub mod afs; - -#[cfg(feature = "ninep")] -pub mod ninep; - -#[cfg(feature = "sftp")] -pub mod sftp; - -#[cfg(feature = "nfs")] -pub mod nfs; - -#[cfg(feature = "smb")] -pub mod smb; - -#[cfg(feature = "syncthing")] -pub mod syncthing; - -#[cfg(feature = "ceph")] -pub mod ceph; - -pub use local::LocalBackend; - -// Re-export provider types when features are enabled -#[cfg(feature = "dropbox")] -pub use dropbox::{DropboxBackend, DropboxConfig, DropboxTokens}; - -#[cfg(feature = "gdrive")] -pub use gdrive::{GoogleDriveBackend, GoogleDriveConfig, GoogleTokens}; - -#[cfg(feature = "onedrive")] -pub use onedrive::{OneDriveBackend, OneDriveConfig, OneDriveTokens}; - -#[cfg(feature = "box")] -pub use box_com::{BoxBackend, BoxConfig, BoxTokens}; - -#[cfg(feature = "s3")] -pub use s3::{S3Backend, S3Config}; - -#[cfg(feature = "ipfs")] -pub use ipfs::{IpfsBackend, IpfsConfig}; - -#[cfg(feature = "webdav")] -pub use webdav::{WebDavBackend, WebDavConfig, WebDavAuth}; - -#[cfg(feature = "afs")] -pub use afs::{AfsBackend, AfsConfig}; - -#[cfg(feature = "ninep")] -pub use ninep::{NinePBackend, NinePConfig}; - -#[cfg(feature = "sftp")] -pub use sftp::{SftpBackend, SftpConfig, SftpAuth}; - -#[cfg(feature = "nfs")] -pub use nfs::{NfsBackend, NfsConfig, NfsVersion}; - -#[cfg(feature = "smb")] -pub use smb::{SmbBackend, SmbConfig, SmbVersion, SmbAuth}; - -#[cfg(feature = "syncthing")] -pub use syncthing::{SyncthingBackend, SyncthingConfig}; - -#[cfg(feature = "ceph")] -pub use ceph::{CephBackend, CephConfig, CephMode}; - -use cfk_core::{StorageBackend, CfkResult, CfkError}; -use std::collections::HashMap; -use std::sync::Arc; - -/// Registry of storage backends -pub struct BackendRegistry { - backends: HashMap>, -} - -impl BackendRegistry { - pub fn new() -> Self { - Self { backends: HashMap::new() } - } - - pub fn register(&mut self, backend: Arc) { - self.backends.insert(backend.id().to_string(), backend); - } - - pub fn get(&self, id: &str) -> Option> { - self.backends.get(id).cloned() - } - - pub fn get_or_err(&self, id: &str) -> CfkResult> { - self.get(id).ok_or_else(|| CfkError::BackendNotFound(id.to_string())) - } - - pub fn list(&self) -> Vec<&str> { - self.backends.keys().map(|s| s.as_str()).collect() - } - - pub fn remove(&mut self, id: &str) -> Option> { - self.backends.remove(id) - } -} - -impl Default for BackendRegistry { - fn default() -> Self { - Self::new() - } -} diff --git a/czech-file-knife/cfk-providers/src/local.rs b/czech-file-knife/cfk-providers/src/local.rs deleted file mode 100644 index 941652493..000000000 --- a/czech-file-knife/cfk-providers/src/local.rs +++ /dev/null @@ -1,484 +0,0 @@ -//! Local filesystem backend - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - backend::{ByteStream, SpaceInfo, StorageBackend, StorageCapabilities}, - entry::{DirectoryListing, Entry, EntryKind}, - error::{CfkError, CfkResult}, - metadata::{Metadata, Permissions}, - operations::*, - VirtualPath, -}; -use std::path::{Path, PathBuf}; -use tokio::fs; -use tokio::io::AsyncReadExt; - -/// Local filesystem backend -pub struct LocalBackend { - id: String, - root: PathBuf, - capabilities: StorageCapabilities, -} - -impl LocalBackend { - pub fn new(id: impl Into, root: impl AsRef) -> Self { - Self { - id: id.into(), - root: root.as_ref().to_path_buf(), - capabilities: StorageCapabilities::local_filesystem(), - } - } - - fn to_real_path(&self, path: &VirtualPath) -> PathBuf { - let mut real = self.root.clone(); - for seg in &path.segments { - real.push(seg); - } - real - } - - fn to_virtual_path(&self, real: &Path) -> CfkResult { - let relative = real - .strip_prefix(&self.root) - .map_err(|_| CfkError::InvalidPath(real.display().to_string()))?; - Ok(VirtualPath::new(&self.id, relative.to_string_lossy())) - } - - async fn metadata_from_path(&self, path: &Path) -> CfkResult<(EntryKind, Metadata)> { - let meta = fs::metadata(path).await?; - let kind = if meta.is_dir() { - EntryKind::Directory - } else if meta.is_file() { - EntryKind::File - } else if meta.is_symlink() { - EntryKind::Symlink - } else { - EntryKind::Unknown - }; - - let mut metadata = Metadata::new(); - metadata.size = Some(meta.len()); - - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - metadata.permissions = Some(Permissions::new(meta.mode())); - } - - if let Ok(modified) = meta.modified() { - metadata.modified = Some(modified.into()); - } - if let Ok(created) = meta.created() { - metadata.created = Some(created.into()); - } - - Ok((kind, metadata)) - } -} - -#[async_trait] -impl StorageBackend for LocalBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Local Filesystem" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.root.exists() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let real = self.to_real_path(path); - if !real.exists() { - return Err(CfkError::NotFound(path.to_string())); - } - let (kind, metadata) = self.metadata_from_path(&real).await?; - Ok(Entry { path: path.clone(), kind, metadata }) - } - - async fn list_directory(&self, path: &VirtualPath, _options: &ListOptions) -> CfkResult { - let real = self.to_real_path(path); - if !real.is_dir() { - return Err(CfkError::NotADirectory(path.to_string())); - } - - let mut entries = Vec::new(); - let mut read_dir = fs::read_dir(&real).await?; - - while let Some(entry) = read_dir.next_entry().await? { - let entry_path = entry.path(); - let vpath = self.to_virtual_path(&entry_path)?; - let (kind, metadata) = self.metadata_from_path(&entry_path).await?; - entries.push(Entry { path: vpath, kind, metadata }); - } - - Ok(DirectoryListing::new(path.clone(), entries)) - } - - async fn read_file(&self, path: &VirtualPath, options: &ReadOptions) -> CfkResult { - let real = self.to_real_path(path); - if !real.is_file() { - return Err(CfkError::NotAFile(path.to_string())); - } - - let mut file = fs::File::open(&real).await?; - let mut buffer = Vec::new(); - - if let Some((start, end)) = options.range { - use tokio::io::AsyncSeekExt; - file.seek(std::io::SeekFrom::Start(start)).await?; - let len = (end - start) as usize; - buffer.resize(len, 0); - file.read_exact(&mut buffer).await?; - } else { - file.read_to_end(&mut buffer).await?; - } - - let bytes = Bytes::from(buffer); - Ok(Box::pin(futures::stream::once(async { Ok(bytes) }))) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes, options: &WriteOptions) -> CfkResult { - let real = self.to_real_path(path); - - if real.exists() && !options.overwrite { - return Err(CfkError::AlreadyExists(path.to_string())); - } - - if options.create_parents { - if let Some(parent) = real.parent() { - fs::create_dir_all(parent).await?; - } - } - - fs::write(&real, &data).await?; - self.get_metadata(path).await - } - - async fn write_file_stream(&self, path: &VirtualPath, mut stream: ByteStream, _size_hint: Option, options: &WriteOptions) -> CfkResult { - use futures::StreamExt; - - let mut data = Vec::new(); - while let Some(chunk) = stream.next().await { - data.extend_from_slice(&chunk?); - } - self.write_file(path, Bytes::from(data), options).await - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let real = self.to_real_path(path); - fs::create_dir_all(&real).await?; - self.get_metadata(path).await - } - - async fn delete(&self, path: &VirtualPath, options: &DeleteOptions) -> CfkResult<()> { - let real = self.to_real_path(path); - - if !real.exists() { - if options.force { - return Ok(()); - } - return Err(CfkError::NotFound(path.to_string())); - } - - if real.is_dir() { - if options.recursive { - fs::remove_dir_all(&real).await?; - } else { - fs::remove_dir(&real).await?; - } - } else { - fs::remove_file(&real).await?; - } - Ok(()) - } - - async fn copy(&self, source: &VirtualPath, dest: &VirtualPath, options: &CopyOptions) -> CfkResult { - let src_real = self.to_real_path(source); - let dst_real = self.to_real_path(dest); - - if !src_real.exists() { - return Err(CfkError::NotFound(source.to_string())); - } - if dst_real.exists() && !options.overwrite { - return Err(CfkError::AlreadyExists(dest.to_string())); - } - - fs::copy(&src_real, &dst_real).await?; - self.get_metadata(dest).await - } - - async fn rename(&self, source: &VirtualPath, dest: &VirtualPath, options: &MoveOptions) -> CfkResult { - let src_real = self.to_real_path(source); - let dst_real = self.to_real_path(dest); - - if !src_real.exists() { - return Err(CfkError::NotFound(source.to_string())); - } - if dst_real.exists() && !options.overwrite { - return Err(CfkError::AlreadyExists(dest.to_string())); - } - - fs::rename(&src_real, &dst_real).await?; - self.get_metadata(dest).await - } - - async fn get_space_info(&self) -> CfkResult { - #[cfg(unix)] - { - use std::ffi::CString; - use std::mem::MaybeUninit; - - let path_cstr = CString::new(self.root.to_string_lossy().as_bytes()) - .map_err(|_| CfkError::InvalidPath(self.root.display().to_string()))?; - - let mut stat: MaybeUninit = MaybeUninit::uninit(); - let result = unsafe { libc::statvfs(path_cstr.as_ptr(), stat.as_mut_ptr()) }; - - if result == 0 { - let stat = unsafe { stat.assume_init() }; - let block_size = stat.f_frsize as u64; - let total = stat.f_blocks as u64 * block_size; - let available = stat.f_bavail as u64 * block_size; - let free = stat.f_bfree as u64 * block_size; - let used = total - free; - - Ok(SpaceInfo { - total: Some(total), - used: Some(used), - available: Some(available), - }) - } else { - Ok(SpaceInfo::unknown()) - } - } - - #[cfg(not(unix))] - { - Ok(SpaceInfo::unknown()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::StreamExt; - use tempfile::TempDir; - - fn make_backend(dir: &TempDir) -> LocalBackend { - LocalBackend::new("test", dir.path()) - } - - fn make_path(backend: &LocalBackend, p: &str) -> VirtualPath { - VirtualPath::new(backend.id(), p) - } - - #[tokio::test] - async fn test_backend_properties() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - - assert_eq!(backend.id(), "test"); - assert_eq!(backend.display_name(), "Local Filesystem"); - assert!(backend.is_available().await); - assert!(backend.capabilities().read); - assert!(backend.capabilities().write); - } - - #[tokio::test] - async fn test_create_and_read_file() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/test.txt"); - - // Write file - let data = Bytes::from("Hello, World!"); - let options = WriteOptions { overwrite: true, ..Default::default() }; - let entry = backend.write_file(&path, data.clone(), &options).await.unwrap(); - - assert!(entry.is_file()); - assert_eq!(entry.name(), Some("test.txt")); - - // Read file - let read_opts = ReadOptions::default(); - let mut stream = backend.read_file(&path, &read_opts).await.unwrap(); - let mut content = Vec::new(); - while let Some(chunk) = stream.next().await { - content.extend_from_slice(&chunk.unwrap()); - } - assert_eq!(content, b"Hello, World!"); - } - - #[tokio::test] - async fn test_write_without_overwrite_fails() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/test.txt"); - - // First write succeeds - let options = WriteOptions { overwrite: false, ..Default::default() }; - backend.write_file(&path, Bytes::from("first"), &options).await.unwrap(); - - // Second write should fail - let result = backend.write_file(&path, Bytes::from("second"), &options).await; - assert!(matches!(result, Err(CfkError::AlreadyExists(_)))); - } - - #[tokio::test] - async fn test_create_directory() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/subdir/nested"); - - let entry = backend.create_directory(&path).await.unwrap(); - assert!(entry.is_directory()); - } - - #[tokio::test] - async fn test_list_directory() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - - // Create some files and dirs - backend.write_file( - &make_path(&backend, "/file1.txt"), - Bytes::from("content1"), - &WriteOptions { overwrite: true, ..Default::default() }, - ).await.unwrap(); - - backend.write_file( - &make_path(&backend, "/file2.txt"), - Bytes::from("content2"), - &WriteOptions { overwrite: true, ..Default::default() }, - ).await.unwrap(); - - backend.create_directory(&make_path(&backend, "/subdir")).await.unwrap(); - - // List root - let listing = backend - .list_directory(&VirtualPath::root("test"), &ListOptions::default()) - .await - .unwrap(); - - assert_eq!(listing.entries.len(), 3); - let names: Vec<_> = listing.entries.iter().filter_map(|e| e.name()).collect(); - assert!(names.contains(&"file1.txt")); - assert!(names.contains(&"file2.txt")); - assert!(names.contains(&"subdir")); - } - - #[tokio::test] - async fn test_delete_file() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/to_delete.txt"); - - backend.write_file(&path, Bytes::from("delete me"), &WriteOptions::default()).await.unwrap(); - - // Delete - backend.delete(&path, &DeleteOptions::default()).await.unwrap(); - - // Should not exist now - let result = backend.get_metadata(&path).await; - assert!(matches!(result, Err(CfkError::NotFound(_)))); - } - - #[tokio::test] - async fn test_delete_nonexistent_with_force() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/nonexistent.txt"); - - // Without force - should fail - let result = backend.delete(&path, &DeleteOptions::default()).await; - assert!(matches!(result, Err(CfkError::NotFound(_)))); - - // With force - should succeed - let options = DeleteOptions { force: true, ..Default::default() }; - backend.delete(&path, &options).await.unwrap(); - } - - #[tokio::test] - async fn test_copy_file() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let src = make_path(&backend, "/original.txt"); - let dst = make_path(&backend, "/copied.txt"); - - backend.write_file(&src, Bytes::from("original content"), &WriteOptions::default()).await.unwrap(); - - // Copy - let entry = backend.copy(&src, &dst, &CopyOptions::default()).await.unwrap(); - assert!(entry.is_file()); - assert_eq!(entry.name(), Some("copied.txt")); - - // Verify content - let mut stream = backend.read_file(&dst, &ReadOptions::default()).await.unwrap(); - let mut content = Vec::new(); - while let Some(chunk) = stream.next().await { - content.extend_from_slice(&chunk.unwrap()); - } - assert_eq!(content, b"original content"); - } - - #[tokio::test] - async fn test_rename_file() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let src = make_path(&backend, "/old_name.txt"); - let dst = make_path(&backend, "/new_name.txt"); - - backend.write_file(&src, Bytes::from("content"), &WriteOptions::default()).await.unwrap(); - - // Rename - let entry = backend.rename(&src, &dst, &MoveOptions::default()).await.unwrap(); - assert!(entry.is_file()); - assert_eq!(entry.name(), Some("new_name.txt")); - - // Old path should not exist - let result = backend.get_metadata(&src).await; - assert!(matches!(result, Err(CfkError::NotFound(_)))); - } - - #[tokio::test] - async fn test_read_file_range() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/ranged.txt"); - - backend.write_file(&path, Bytes::from("0123456789"), &WriteOptions::default()).await.unwrap(); - - // Read range 3-7 (bytes 3, 4, 5, 6) - let options = ReadOptions { range: Some((3, 7)), ..Default::default() }; - let mut stream = backend.read_file(&path, &options).await.unwrap(); - let mut content = Vec::new(); - while let Some(chunk) = stream.next().await { - content.extend_from_slice(&chunk.unwrap()); - } - assert_eq!(content, b"3456"); - } - - #[tokio::test] - async fn test_get_metadata() { - let tmp = TempDir::new().unwrap(); - let backend = make_backend(&tmp); - let path = make_path(&backend, "/meta_test.txt"); - - let content = "Test content for metadata"; - backend.write_file(&path, Bytes::from(content), &WriteOptions::default()).await.unwrap(); - - let entry = backend.get_metadata(&path).await.unwrap(); - assert!(entry.is_file()); - assert_eq!(entry.size(), Some(content.len() as u64)); - assert!(entry.metadata.modified.is_some()); - } -} diff --git a/czech-file-knife/cfk-providers/src/nfs.rs b/czech-file-knife/cfk-providers/src/nfs.rs deleted file mode 100644 index 5bcd36b57..000000000 --- a/czech-file-knife/cfk-providers/src/nfs.rs +++ /dev/null @@ -1,389 +0,0 @@ -//! NFS storage backend -//! -//! Network File System client implementation. -//! Supports NFSv3 and NFSv4 protocols. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use std::path::PathBuf; - -/// NFS version -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NfsVersion { - V3, - V4, - V41, -} - -impl Default for NfsVersion { - fn default() -> Self { - Self::V4 - } -} - -/// NFS authentication flavor -#[derive(Debug, Clone)] -pub enum NfsAuth { - /// AUTH_SYS (Unix authentication) - Sys { uid: u32, gid: u32, gids: Vec }, - /// AUTH_NONE - None, - /// RPCSEC_GSS (Kerberos) - Gss { principal: String }, -} - -impl Default for NfsAuth { - fn default() -> Self { - Self::Sys { - uid: 65534, // nobody - gid: 65534, - gids: vec![], - } - } -} - -/// NFS backend configuration -#[derive(Debug, Clone)] -pub struct NfsConfig { - /// Server hostname or IP - pub server: String, - /// Export path - pub export: String, - /// NFS version - pub version: NfsVersion, - /// Authentication - pub auth: NfsAuth, - /// Read size (NFSv3: 65536, NFSv4: 1MB) - pub rsize: u32, - /// Write size - pub wsize: u32, - /// Use TCP (vs UDP for NFSv3) - pub tcp: bool, - /// Port (0 = use portmapper/rpcbind) - pub port: u16, -} - -impl Default for NfsConfig { - fn default() -> Self { - Self { - server: "localhost".to_string(), - export: "/".to_string(), - version: NfsVersion::V4, - auth: NfsAuth::default(), - rsize: 1048576, // 1MB - wsize: 1048576, - tcp: true, - port: 2049, - } - } -} - -/// NFS file handle -#[derive(Debug, Clone, Default)] -struct NfsFileHandle { - data: Vec, -} - -/// NFS storage backend -/// -/// Note: This is a stub implementation. Full implementation would require -/// ONC RPC and XDR encoding, which is complex. Consider using `nfs` crate -/// or system mount. -pub struct NfsBackend { - id: String, - config: NfsConfig, - capabilities: StorageCapabilities, - /// Root file handle (obtained from MOUNT/PUTROOTFH) - root_fh: Option, -} - -impl NfsBackend { - pub fn new(id: impl Into, config: NfsConfig) -> Self { - Self { - id: id.into(), - config, - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: false, - list: true, - search: false, - versioning: false, - sharing: true, // ACLs - streaming: true, - resume: true, - watch: false, // NFSv4.1 has callbacks - metadata: true, - thumbnails: false, - max_file_size: None, - }, - root_fh: None, - } - } - - /// Create from NFS URL: nfs://server/export - pub fn from_url(id: impl Into, url: &str) -> CfkResult { - let parsed = url::Url::parse(url) - .map_err(|e| CfkError::InvalidPath(format!("Invalid URL: {}", e)))?; - - if parsed.scheme() != "nfs" { - return Err(CfkError::InvalidPath("URL scheme must be nfs".into())); - } - - let server = parsed - .host_str() - .ok_or_else(|| CfkError::InvalidPath("Missing server".into()))? - .to_string(); - - let export = parsed.path().to_string(); - let port = parsed.port().unwrap_or(2049); - - Ok(Self::new( - id, - NfsConfig { - server, - export, - port, - ..Default::default() - }, - )) - } - - /// Mount the NFS export - pub async fn mount(&mut self) -> CfkResult<()> { - // In a full implementation: - // 1. For NFSv3: Contact portmapper, get MOUNT port, call MOUNT - // 2. For NFSv4: Use PUTROOTFH compound operation - - match self.config.version { - NfsVersion::V3 => { - // NFSv3 mount protocol - // 1. RPC call to rpcbind to get mount daemon port - // 2. RPC MOUNT call to get root file handle - } - NfsVersion::V4 | NfsVersion::V41 => { - // NFSv4 uses COMPOUND operations - // PUTROOTFH + GETFH to get root handle - } - } - - Err(CfkError::Unsupported( - "NFS backend is a stub. Use system mount or nfs crate.".into(), - )) - } - - /// Convert VirtualPath to NFS path components - fn to_path_components(&self, path: &VirtualPath) -> Vec { - path.segments.clone() - } - - /// Lookup a path and return file handle - async fn lookup(&self, _path: &VirtualPath) -> CfkResult { - // Would use LOOKUP (v3) or LOOKUP in COMPOUND (v4) - Err(CfkError::Unsupported("NFS stub".into())) - } -} - -#[async_trait] -impl StorageBackend for NfsBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - match self.config.version { - NfsVersion::V3 => "NFSv3", - NfsVersion::V4 => "NFSv4", - NfsVersion::V41 => "NFSv4.1", - } - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.root_fh.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let _fh = self.lookup(path).await?; - // Would use GETATTR operation - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let _fh = self.lookup(path).await?; - // Would use READDIR/READDIRPLUS (v3) or READDIR in COMPOUND (v4) - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let _fh = self.lookup(path).await?; - // Would use READ operation with offset/count - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn write_file(&self, path: &VirtualPath, _data: Bytes) -> CfkResult { - // Would use CREATE + WRITE operations - let _components = self.to_path_components(path); - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let _fh = self.lookup(path).await?; - // Would use REMOVE (file) or RMDIR (directory) - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - // Would use MKDIR operation - let _components = self.to_path_components(path); - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - // NFS doesn't have native copy (until NFSv4.2 COPY operation) - Err(CfkError::Unsupported("NFS doesn't support native copy".into())) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - // Would use RENAME operation - let _from_components = self.to_path_components(from); - let _to_components = self.to_path_components(to); - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // Would use FSSTAT (v3) or GETATTR with fsinfo (v4) - - Err(CfkError::Unsupported("NFS stub - use system mount".into())) - } -} - -/// NFS file types -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NfsFileType { - Regular = 1, - Directory = 2, - BlockDevice = 3, - CharDevice = 4, - Symlink = 5, - Socket = 6, - Fifo = 7, -} - -/// NFS file attributes (fattr3/fattr4) -#[derive(Debug, Clone, Default)] -pub struct NfsAttributes { - pub file_type: u32, - pub mode: u32, - pub nlink: u32, - pub uid: u32, - pub gid: u32, - pub size: u64, - pub used: u64, - pub fsid: u64, - pub fileid: u64, - pub atime_sec: u32, - pub atime_nsec: u32, - pub mtime_sec: u32, - pub mtime_nsec: u32, - pub ctime_sec: u32, - pub ctime_nsec: u32, -} - -impl NfsAttributes { - pub fn to_entry(&self, backend_id: &str, path: &str) -> Entry { - let kind = match self.file_type { - 2 => EntryKind::Directory, - 5 => EntryKind::Symlink, - _ => EntryKind::File, - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(self.size); - metadata.permissions = Some(self.mode); - metadata.uid = Some(self.uid); - metadata.gid = Some(self.gid); - - if self.mtime_sec > 0 { - metadata.modified = chrono::DateTime::from_timestamp( - self.mtime_sec as i64, - self.mtime_nsec, - ); - } - - Entry { - path: VirtualPath::new(backend_id, path), - kind, - metadata, - } - } -} - -/// Helper function to use system NFS mount -impl NfsBackend { - /// Mount using system mount command (requires root or fuse-nfs) - pub fn mount_system(&self, mount_point: &PathBuf) -> CfkResult<()> { - use std::process::Command; - - let source = format!("{}:{}", self.config.server, self.config.export); - let version = match self.config.version { - NfsVersion::V3 => "3", - NfsVersion::V4 => "4", - NfsVersion::V41 => "4.1", - }; - - let status = Command::new("mount") - .args([ - "-t", "nfs", - "-o", &format!("vers={}", version), - &source, - mount_point.to_str().unwrap_or("/mnt"), - ]) - .status() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !status.success() { - return Err(CfkError::ProviderApi { - provider: "nfs".into(), - message: "mount command failed".into(), - }); - } - - Ok(()) - } - - /// Unmount system mount - pub fn unmount_system(&self, mount_point: &PathBuf) -> CfkResult<()> { - use std::process::Command; - - let status = Command::new("umount") - .arg(mount_point.to_str().unwrap_or("/mnt")) - .status() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !status.success() { - return Err(CfkError::ProviderApi { - provider: "nfs".into(), - message: "umount command failed".into(), - }); - } - - Ok(()) - } -} diff --git a/czech-file-knife/cfk-providers/src/ninep.rs b/czech-file-knife/cfk-providers/src/ninep.rs deleted file mode 100644 index 2206bf5c2..000000000 --- a/czech-file-knife/cfk-providers/src/ninep.rs +++ /dev/null @@ -1,960 +0,0 @@ -//! 9P/Plan 9 filesystem protocol backend -//! -//! Used in WSL2 (drvfs), QEMU/KVM (virtio-9p), and Plan 9/Inferno systems. -//! Implements 9P2000.L (Linux extensions) protocol. - -use async_trait::async_trait; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use std::collections::HashMap; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Arc; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::sync::RwLock; - -/// 9P message types -mod msg { - pub const TVERSION: u8 = 100; - pub const RVERSION: u8 = 101; - pub const TAUTH: u8 = 102; - pub const RAUTH: u8 = 103; - pub const TATTACH: u8 = 104; - pub const RATTACH: u8 = 105; - pub const RERROR: u8 = 107; - pub const TFLUSH: u8 = 108; - pub const RFLUSH: u8 = 109; - pub const TWALK: u8 = 110; - pub const RWALK: u8 = 111; - pub const TOPEN: u8 = 112; - pub const ROPEN: u8 = 113; - pub const TCREATE: u8 = 114; - pub const RCREATE: u8 = 115; - pub const TREAD: u8 = 116; - pub const RREAD: u8 = 117; - pub const TWRITE: u8 = 118; - pub const RWRITE: u8 = 119; - pub const TCLUNK: u8 = 120; - pub const RCLUNK: u8 = 121; - pub const TREMOVE: u8 = 122; - pub const RREMOVE: u8 = 123; - pub const TSTAT: u8 = 124; - pub const RSTAT: u8 = 125; - pub const TWSTAT: u8 = 126; - pub const RWSTAT: u8 = 127; - - // 9P2000.L extensions - pub const TLOPEN: u8 = 12; - pub const RLOPEN: u8 = 13; - pub const TLCREATE: u8 = 14; - pub const RLCREATE: u8 = 15; - pub const TREADDIR: u8 = 40; - pub const RREADDIR: u8 = 41; - pub const TGETATTR: u8 = 24; - pub const RGETATTR: u8 = 25; -} - -/// Open modes -mod omode { - pub const READ: u8 = 0; - pub const WRITE: u8 = 1; - pub const RDWR: u8 = 2; - pub const TRUNC: u16 = 0x10; -} - -/// 9P QID type -#[derive(Debug, Clone, Default)] -struct Qid { - qid_type: u8, - version: u32, - path: u64, -} - -impl Qid { - fn is_dir(&self) -> bool { - self.qid_type & 0x80 != 0 - } -} - -/// 9P backend configuration -#[derive(Debug, Clone)] -pub struct NinePConfig { - /// Server address (host:port) - pub address: String, - /// Attach name (usually empty or a mount tag) - pub aname: String, - /// Username for authentication - pub uname: String, - /// Maximum message size - pub msize: u32, -} - -impl Default for NinePConfig { - fn default() -> Self { - Self { - address: "127.0.0.1:564".to_string(), - aname: String::new(), - uname: "nobody".to_string(), - msize: 8192, - } - } -} - -/// 9P connection state -struct Connection { - stream: TcpStream, - msize: u32, - root_fid: u32, -} - -/// 9P storage backend -pub struct NinePBackend { - id: String, - config: NinePConfig, - connection: Arc>>, - fid_counter: AtomicU32, - capabilities: StorageCapabilities, - /// Cache of path to fid mapping - fid_cache: Arc>>, -} - -impl NinePBackend { - pub fn new(id: impl Into, config: NinePConfig) -> Self { - Self { - id: id.into(), - config, - connection: Arc::new(RwLock::new(None)), - fid_counter: AtomicU32::new(1), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: false, // 9P doesn't have native copy - list: true, - search: false, - versioning: false, - sharing: false, - streaming: true, - resume: false, - watch: false, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - fid_cache: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Connect to 9P server - pub async fn connect(&self) -> CfkResult<()> { - let stream = TcpStream::connect(&self.config.address) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let mut conn = Connection { - stream, - msize: self.config.msize, - root_fid: 0, - }; - - // Send Tversion - let tag = 0xFFFF; // NOTAG for version - let mut buf = BytesMut::new(); - buf.put_u32_le(0); // size placeholder - buf.put_u8(msg::TVERSION); - buf.put_u16_le(tag); - buf.put_u32_le(self.config.msize); - put_string(&mut buf, "9P2000.L"); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - // Read Rversion - let reply = read_message(&mut conn.stream).await?; - if reply[4] != msg::RVERSION { - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: "Version negotiation failed".into(), - }); - } - - let mut cursor = &reply[7..]; - conn.msize = cursor.get_u32_le(); - - // Send Tattach - let root_fid = self.alloc_fid(); - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TATTACH); - buf.put_u16_le(1); // tag - buf.put_u32_le(root_fid); - buf.put_u32_le(0xFFFFFFFF); // afid (no auth) - put_string(&mut buf, &self.config.uname); - put_string(&mut buf, &self.config.aname); - buf.put_u32_le(0); // n_uname (9P2000.L) - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - conn.root_fid = root_fid; - *self.connection.write().await = Some(conn); - - Ok(()) - } - - /// Allocate a new fid - fn alloc_fid(&self) -> u32 { - self.fid_counter.fetch_add(1, Ordering::SeqCst) - } - - /// Walk to a path and return the fid - async fn walk(&self, path: &VirtualPath) -> CfkResult { - let path_str = path.to_string(); - - // Check cache - { - let cache = self.fid_cache.read().await; - if let Some(&fid) = cache.get(&path_str) { - return Ok(fid); - } - } - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - let new_fid = self.alloc_fid(); - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TWALK); - buf.put_u16_le(2); // tag - buf.put_u32_le(conn.root_fid); - buf.put_u32_le(new_fid); - - // Path segments - let segments = &path.segments; - buf.put_u16_le(segments.len() as u16); - for seg in segments { - put_string(&mut buf, seg); - } - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::NotFound(format!("{}: {}", path, error))); - } - - // Cache the fid - { - let mut cache = self.fid_cache.write().await; - cache.insert(path_str, new_fid); - } - - Ok(new_fid) - } - - /// Clunk (release) a fid - async fn clunk(&self, fid: u32) -> CfkResult<()> { - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TCLUNK); - buf.put_u16_le(3); - buf.put_u32_le(fid); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let _reply = read_message(&mut conn.stream).await?; - Ok(()) - } - - /// Get file attributes (9P2000.L Tgetattr) - async fn getattr(&self, fid: u32) -> CfkResult { - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TGETATTR); - buf.put_u16_le(4); - buf.put_u32_le(fid); - buf.put_u64_le(0x7FF); // request_mask: all basic attrs - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - parse_getattr(&reply) - } -} - -/// File attributes -#[derive(Debug, Clone, Default)] -struct FileAttr { - mode: u32, - uid: u32, - gid: u32, - nlink: u64, - size: u64, - atime_sec: u64, - mtime_sec: u64, - ctime_sec: u64, -} - -/// Parse Rgetattr response -fn parse_getattr(data: &[u8]) -> CfkResult { - if data.len() < 100 { - return Err(CfkError::Serialization("Rgetattr too short".into())); - } - - let mut cursor = &data[7..]; // Skip size, type, tag - let _valid = cursor.get_u64_le(); - let _qid_type = cursor.get_u8(); - let _qid_version = cursor.get_u32_le(); - let _qid_path = cursor.get_u64_le(); - - Ok(FileAttr { - mode: cursor.get_u32_le(), - uid: cursor.get_u32_le(), - gid: cursor.get_u32_le(), - nlink: cursor.get_u64_le(), - _rdev: cursor.get_u64_le(), - size: cursor.get_u64_le(), - _blksize: cursor.get_u64_le(), - _blocks: cursor.get_u64_le(), - atime_sec: cursor.get_u64_le(), - _atime_nsec: cursor.get_u64_le(), - mtime_sec: cursor.get_u64_le(), - _mtime_nsec: cursor.get_u64_le(), - ctime_sec: cursor.get_u64_le(), - ..Default::default() - }) -} - -/// Parse error from Rerror message -fn parse_error(data: &[u8]) -> CfkResult { - if data.len() < 9 { - return Ok("Unknown error".into()); - } - - let mut cursor = &data[7..]; - let len = cursor.get_u16_le() as usize; - if cursor.len() >= len { - Ok(String::from_utf8_lossy(&cursor[..len]).to_string()) - } else { - Ok("Unknown error".into()) - } -} - -/// Read a 9P message -async fn read_message(stream: &mut TcpStream) -> CfkResult> { - let mut size_buf = [0u8; 4]; - stream - .read_exact(&mut size_buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let size = u32::from_le_bytes(size_buf) as usize; - if size < 4 || size > 1024 * 1024 { - return Err(CfkError::Serialization("Invalid message size".into())); - } - - let mut buf = vec![0u8; size]; - buf[0..4].copy_from_slice(&size_buf); - - stream - .read_exact(&mut buf[4..]) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - Ok(buf) -} - -/// Write string in 9P format (2-byte length prefix) -fn put_string(buf: &mut BytesMut, s: &str) { - let bytes = s.as_bytes(); - buf.put_u16_le(bytes.len() as u16); - buf.put_slice(bytes); -} - -#[async_trait] -impl StorageBackend for NinePBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "9P" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - let conn = self.connection.read().await; - conn.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let fid = self.walk(path).await?; - let attr = self.getattr(fid).await?; - - let kind = if (attr.mode & 0o40000) != 0 { - EntryKind::Directory - } else if (attr.mode & 0o120000) == 0o120000 { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(attr.size); - metadata.permissions = Some(attr.mode); - metadata.uid = Some(attr.uid); - metadata.gid = Some(attr.gid); - - if attr.mtime_sec > 0 { - metadata.modified = chrono::DateTime::from_timestamp(attr.mtime_sec as i64, 0); - } - - Ok(Entry { - path: path.clone(), - kind, - metadata, - }) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let fid = self.walk(path).await?; - - // Open directory for reading - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - // Tlopen - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TLOPEN); - buf.put_u16_le(5); - buf.put_u32_le(fid); - buf.put_u32_le(omode::READ as u32); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - // Treaddir - let mut entries = Vec::new(); - let mut offset = 0u64; - - loop { - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TREADDIR); - buf.put_u16_le(6); - buf.put_u32_le(fid); - buf.put_u64_le(offset); - buf.put_u32_le(conn.msize - 24); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - break; - } - - let mut cursor = &reply[7..]; - let count = cursor.get_u32_le() as usize; - if count == 0 { - break; - } - - // Parse directory entries - let data = &cursor[..count]; - let mut pos = 0; - - while pos < data.len() { - if pos + 24 > data.len() { - break; - } - - let mut entry_cursor = &data[pos..]; - let qid_type = entry_cursor.get_u8(); - let _qid_version = entry_cursor.get_u32_le(); - let _qid_path = entry_cursor.get_u64_le(); - offset = entry_cursor.get_u64_le(); - let dtype = entry_cursor.get_u8(); - let name_len = entry_cursor.get_u16_le() as usize; - - if pos + 24 + name_len > data.len() { - break; - } - - let name = String::from_utf8_lossy(&entry_cursor[..name_len]).to_string(); - pos += 24 + name_len; - - if name == "." || name == ".." { - continue; - } - - let kind = if qid_type & 0x80 != 0 { - EntryKind::Directory - } else { - EntryKind::File - }; - - let entry_path = if path.segments.is_empty() { - VirtualPath::new(&self.id, &name) - } else { - VirtualPath::new(&self.id, &format!("{}/{}", path.segments.join("/"), name)) - }; - - entries.push(Entry { - path: entry_path, - kind, - metadata: Metadata::default(), - }); - } - } - - drop(conn_guard); - self.clunk(fid).await?; - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let fid = self.walk(path).await?; - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - // Tlopen - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TLOPEN); - buf.put_u16_le(7); - buf.put_u32_le(fid); - buf.put_u32_le(omode::READ as u32); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - // Read file content - let mut content = Vec::new(); - let mut offset = 0u64; - let chunk_size = conn.msize - 24; - - loop { - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TREAD); - buf.put_u16_le(8); - buf.put_u32_le(fid); - buf.put_u64_le(offset); - buf.put_u32_le(chunk_size); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - let mut cursor = &reply[7..]; - let count = cursor.get_u32_le() as usize; - if count == 0 { - break; - } - - content.extend_from_slice(&cursor[..count]); - offset += count as u64; - } - - drop(conn_guard); - self.clunk(fid).await?; - - Ok(Bytes::from(content)) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - // Walk to parent and create file - let parent = if path.segments.len() > 1 { - VirtualPath::new(&self.id, &path.segments[..path.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - - let parent_fid = self.walk(&parent).await?; - let name = path.segments.last().cloned().unwrap_or_default(); - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - // Tlcreate - let new_fid = self.fid_counter.fetch_add(1, Ordering::SeqCst); - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TLCREATE); - buf.put_u16_le(9); - buf.put_u32_le(parent_fid); - put_string(&mut buf, &name); - buf.put_u32_le(omode::RDWR as u32 | omode::TRUNC as u32); - buf.put_u32_le(0o644); // mode - buf.put_u32_le(0); // gid - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - // Write data - let mut offset = 0u64; - let chunk_size = (conn.msize - 24) as usize; - - while offset < data.len() as u64 { - let end = std::cmp::min(offset as usize + chunk_size, data.len()); - let chunk = &data[offset as usize..end]; - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TWRITE); - buf.put_u16_le(10); - buf.put_u32_le(new_fid); - buf.put_u64_le(offset); - buf.put_u32_le(chunk.len() as u32); - buf.put_slice(chunk); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - let mut cursor = &reply[7..]; - let written = cursor.get_u32_le() as u64; - offset += written; - } - - drop(conn_guard); - self.clunk(new_fid).await?; - - self.get_metadata(path).await - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let fid = self.walk(path).await?; - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(msg::TREMOVE); - buf.put_u16_le(11); - buf.put_u32_le(fid); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - // Remove from cache - { - let mut cache = self.fid_cache.write().await; - cache.remove(&path.to_string()); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let parent = if path.segments.len() > 1 { - VirtualPath::new(&self.id, &path.segments[..path.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - - let parent_fid = self.walk(&parent).await?; - let name = path.segments.last().cloned().unwrap_or_default(); - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - // Tmkdir (9P2000.L) - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(72); // Tmkdir - buf.put_u16_le(12); - buf.put_u32_le(parent_fid); - put_string(&mut buf, &name); - buf.put_u32_le(0o755); // mode - buf.put_u32_le(0); // gid - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - drop(conn_guard); - self.get_metadata(path).await - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - Err(CfkError::Unsupported("9P does not support copy".into())) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - // 9P2000.L has Trename - let old_fid = self.walk(from).await?; - let new_parent = if to.segments.len() > 1 { - VirtualPath::new(&self.id, &to.segments[..to.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - let new_parent_fid = self.walk(&new_parent).await?; - let new_name = to.segments.last().cloned().unwrap_or_default(); - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - // Trenameat - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(74); // Trenameat - buf.put_u16_le(13); - buf.put_u32_le(old_fid); - put_string(&mut buf, from.segments.last().unwrap_or(&String::new())); - buf.put_u32_le(new_parent_fid); - put_string(&mut buf, &new_name); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - let error = parse_error(&reply)?; - return Err(CfkError::ProviderApi { - provider: "9p".into(), - message: error, - }); - } - - // Update cache - { - let mut cache = self.fid_cache.write().await; - cache.remove(&from.to_string()); - } - - drop(conn_guard); - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // 9P2000.L has Tstatfs - let root = VirtualPath::new(&self.id, ""); - let fid = self.walk(&root).await?; - - let mut conn_guard = self.connection.write().await; - let conn = conn_guard - .as_mut() - .ok_or_else(|| CfkError::Network("Not connected".into()))?; - - let mut buf = BytesMut::new(); - buf.put_u32_le(0); - buf.put_u8(8); // Tstatfs - buf.put_u16_le(14); - buf.put_u32_le(fid); - - let size = buf.len() as u32; - buf[0..4].copy_from_slice(&size.to_le_bytes()); - - conn.stream - .write_all(&buf) - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let reply = read_message(&mut conn.stream).await?; - if reply[4] == msg::RERROR { - return Ok((0, 0)); - } - - let mut cursor = &reply[7..]; - let _type = cursor.get_u32_le(); - let bsize = cursor.get_u32_le() as u64; - let blocks = cursor.get_u64_le(); - let bfree = cursor.get_u64_le(); - let bavail = cursor.get_u64_le(); - - let total = blocks * bsize; - let available = bavail * bsize; - - Ok((available, total)) - } -} diff --git a/czech-file-knife/cfk-providers/src/onedrive.rs b/czech-file-knife/cfk-providers/src/onedrive.rs deleted file mode 100644 index 82166eb0a..000000000 --- a/czech-file-knife/cfk-providers/src/onedrive.rs +++ /dev/null @@ -1,620 +0,0 @@ -//! OneDrive storage backend -//! -//! Microsoft Graph API implementation for OneDrive Personal and Business. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use oauth2::{ - basic::BasicClient, AuthUrl, ClientId, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, - RedirectUrl, Scope, TokenUrl, -}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tokio::sync::RwLock; - -const MS_AUTH_URL: &str = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; -const MS_TOKEN_URL: &str = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; -const GRAPH_API_URL: &str = "https://graph.microsoft.com/v1.0"; - -/// Microsoft OAuth tokens -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OneDriveTokens { - pub access_token: String, - pub refresh_token: Option, - pub expires_at: Option>, -} - -/// OneDrive backend configuration -#[derive(Debug, Clone)] -pub struct OneDriveConfig { - pub client_id: String, - pub redirect_uri: String, - /// Use OneDrive for Business (SharePoint) instead of personal - pub business: bool, -} - -/// OneDrive storage backend -pub struct OneDriveBackend { - id: String, - config: OneDriveConfig, - tokens: Arc>>, - http: Client, - capabilities: StorageCapabilities, -} - -impl OneDriveBackend { - pub fn new(id: impl Into, config: OneDriveConfig) -> Self { - Self { - id: id.into(), - config, - tokens: Arc::new(RwLock::new(None)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: true, - versioning: true, - sharing: true, - streaming: true, - resume: true, - watch: true, - metadata: true, - thumbnails: true, - max_file_size: Some(250 * 1024 * 1024 * 1024), // 250GB - }, - } - } - - /// Start OAuth 2.0 + PKCE flow - pub fn start_auth(&self) -> (String, PkceCodeVerifier) { - let client = BasicClient::new(ClientId::new(self.config.client_id.clone())) - .set_auth_uri(AuthUrl::new(MS_AUTH_URL.to_string()).unwrap()) - .set_token_uri(TokenUrl::new(MS_TOKEN_URL.to_string()).unwrap()) - .set_redirect_uri(RedirectUrl::new(self.config.redirect_uri.clone()).unwrap()); - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, _csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("Files.ReadWrite.All".to_string())) - .add_scope(Scope::new("offline_access".to_string())) - .set_pkce_challenge(pkce_challenge) - .url(); - - (auth_url.to_string(), pkce_verifier) - } - - /// Complete OAuth flow with authorization code - pub async fn complete_auth( - &self, - code: &str, - verifier: PkceCodeVerifier, - ) -> CfkResult { - let params = [ - ("code", code.to_string()), - ("grant_type", "authorization_code".to_string()), - ("client_id", self.config.client_id.clone()), - ("redirect_uri", self.config.redirect_uri.clone()), - ("code_verifier", verifier.secret().to_string()), - ]; - - let response = self - .http - .post(MS_TOKEN_URL) - .form(¶ms) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::Auth(format!("Token exchange failed: {}", error_text))); - } - - #[derive(Deserialize)] - struct TokenResponse { - access_token: String, - refresh_token: Option, - expires_in: Option, - } - - let token_resp: TokenResponse = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let tokens = OneDriveTokens { - access_token: token_resp.access_token, - refresh_token: token_resp.refresh_token, - expires_at: token_resp - .expires_in - .map(|secs| Utc::now() + chrono::Duration::seconds(secs)), - }; - - *self.tokens.write().await = Some(tokens.clone()); - Ok(tokens) - } - - /// Set tokens directly - pub async fn set_tokens(&self, tokens: OneDriveTokens) { - *self.tokens.write().await = Some(tokens); - } - - /// Get current access token - async fn get_access_token(&self) -> CfkResult { - let tokens = self.tokens.read().await; - tokens - .as_ref() - .map(|t| t.access_token.clone()) - .ok_or_else(|| CfkError::Auth("Not authenticated".into())) - } - - /// Build API path for OneDrive - fn api_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - format!("{}/me/drive/root", GRAPH_API_URL) - } else { - let path_str = path.segments.join("/"); - format!("{}/me/drive/root:/{}", GRAPH_API_URL, path_str) - } - } - - /// Build children API path - fn children_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - format!("{}/me/drive/root/children", GRAPH_API_URL) - } else { - let path_str = path.segments.join("/"); - format!("{}/me/drive/root:/{}:/children", GRAPH_API_URL, path_str) - } - } -} - -/// OneDrive item metadata -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DriveItem { - id: String, - name: String, - size: Option, - created_date_time: Option, - last_modified_date_time: Option, - folder: Option, - file: Option, - parent_reference: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FolderFacet { - child_count: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FileFacet { - mime_type: Option, - hashes: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FileHashes { - quick_xor_hash: Option, - sha1_hash: Option, - sha256_hash: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ParentReference { - path: Option, -} - -impl DriveItem { - fn to_entry(&self, backend_id: &str, base_path: &str) -> Entry { - let path_str = if base_path.is_empty() { - self.name.clone() - } else { - format!("{}/{}", base_path, self.name) - }; - let virtual_path = VirtualPath::new(backend_id, &path_str); - - let kind = if self.folder.is_some() { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = self.size; - - if let Some(ref file) = self.file { - metadata.mime_type = file.mime_type.clone(); - if let Some(ref hashes) = file.hashes { - metadata.checksum = hashes - .sha256_hash - .clone() - .or_else(|| hashes.sha1_hash.clone()); - } - } - - if let Some(ref modified) = self.last_modified_date_time { - if let Ok(dt) = DateTime::parse_from_rfc3339(modified) { - metadata.modified = Some(dt.with_timezone(&Utc)); - } - } - if let Some(ref created) = self.created_date_time { - if let Ok(dt) = DateTime::parse_from_rfc3339(created) { - metadata.created = Some(dt.with_timezone(&Utc)); - } - } - - Entry { - path: virtual_path, - kind, - metadata, - } - } -} - -#[async_trait] -impl StorageBackend for OneDriveBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - if self.config.business { - "OneDrive for Business" - } else { - "OneDrive" - } - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.tokens.read().await.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let url = self.api_path(path); - - let response = self - .http - .get(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - if status == reqwest::StatusCode::NOT_FOUND { - return Err(CfkError::NotFound(path.to_string())); - } - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "onedrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let item: DriveItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if path.segments.len() > 1 { - path.segments[..path.segments.len() - 1].join("/") - } else { - String::new() - }; - - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let url = self.children_path(path); - - let mut entries = Vec::new(); - let mut next_link: Option = Some(url); - - while let Some(url) = next_link.take() { - let response = self - .http - .get(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct ItemList { - value: Vec, - #[serde(rename = "@odata.nextLink")] - next_link: Option, - } - - let list: ItemList = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = path.segments.join("/"); - - for item in list.value { - entries.push(item.to_entry(&self.id, &base_path)); - } - - next_link = list.next_link; - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let url = format!("{}:/content", self.api_path(path)); - - let response = self - .http - .get(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "onedrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let url = format!("{}:/content", self.api_path(path)); - - let response = self - .http - .put(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .header("Content-Type", "application/octet-stream") - .body(data.to_vec()) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "onedrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let item: DriveItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if path.segments.len() > 1 { - path.segments[..path.segments.len() - 1].join("/") - } else { - String::new() - }; - - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let url = self.api_path(path); - - let response = self - .http - .delete(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != reqwest::StatusCode::NO_CONTENT { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "onedrive".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let parent_path = if path.segments.len() > 1 { - VirtualPath::new(&self.id, &path.segments[..path.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - - let name = path.segments.last().cloned().unwrap_or_default(); - let url = self.children_path(&parent_path); - - #[derive(Serialize)] - struct CreateFolder { - name: String, - folder: serde_json::Value, - #[serde(rename = "@microsoft.graph.conflictBehavior")] - conflict_behavior: String, - } - - let body = CreateFolder { - name, - folder: serde_json::json!({}), - conflict_behavior: "fail".to_string(), - }; - - let response = self - .http - .post(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: DriveItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = parent_path.segments.join("/"); - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_url = self.api_path(from); - let to_parent = if to.segments.len() > 1 { - VirtualPath::new(&self.id, &to.segments[..to.segments.len() - 1].join("/")) - } else { - VirtualPath::new(&self.id, "") - }; - let to_name = to.segments.last().cloned().unwrap_or_default(); - - // Get the parent folder's drive item id - let parent_response = self - .http - .get(&self.api_path(&to_parent)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let parent_item: DriveItem = parent_response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - #[derive(Serialize)] - #[serde(rename_all = "camelCase")] - struct CopyRequest { - parent_reference: ParentRef, - name: String, - } - - #[derive(Serialize)] - struct ParentRef { - id: String, - } - - let body = CopyRequest { - parent_reference: ParentRef { id: parent_item.id }, - name: to_name, - }; - - let _response = self - .http - .post(format!("{}:/copy", from_url)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - // Copy is async in OneDrive, return metadata of destination - self.get_metadata(to).await - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let url = self.api_path(from); - let to_name = to.segments.last().cloned().unwrap_or_default(); - - #[derive(Serialize)] - struct RenameRequest { - name: String, - } - - let body = RenameRequest { name: to_name }; - - let response = self - .http - .patch(&url) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let item: DriveItem = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let base_path = if to.segments.len() > 1 { - to.segments[..to.segments.len() - 1].join("/") - } else { - String::new() - }; - - Ok(item.to_entry(&self.id, &base_path)) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - let response = self - .http - .get(format!("{}/me/drive", GRAPH_API_URL)) - .header("Authorization", format!("Bearer {}", self.get_access_token().await?)) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - #[derive(Deserialize)] - struct Drive { - quota: Option, - } - - #[derive(Deserialize)] - struct DriveQuota { - total: Option, - used: Option, - remaining: Option, - } - - let drive: Drive = response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string()))?; - - let quota = drive.quota.unwrap_or(DriveQuota { - total: None, - used: None, - remaining: None, - }); - - let total = quota.total.unwrap_or(0); - let available = quota.remaining.unwrap_or(0); - - Ok((available, total)) - } -} diff --git a/czech-file-knife/cfk-providers/src/protocols.rs b/czech-file-knife/cfk-providers/src/protocols.rs deleted file mode 100644 index 7bd963d80..000000000 --- a/czech-file-knife/cfk-providers/src/protocols.rs +++ /dev/null @@ -1,265 +0,0 @@ -//! Exotic protocol support -//! -//! Additional protocols beyond standard cloud/file systems: -//! - NNTP/NNTPS: Usenet -//! - Gopher/Gopher+: Pre-web protocol -//! - Gemini: Modern minimalist protocol -//! - RTSP: Streaming -//! - BitTorrent: P2P file sharing -//! - DAT/Hypercore: P2P versioned data -//! - Freenet: Anonymous storage -//! - I2P: Anonymous network -//! - Tor: Onion services -//! - MTP: Mobile devices -//! - AFP: Apple Filing -//! - DLNA/UPnP: Media discovery - -use cfk_core::{CfkError, CfkResult}; - -/// Protocol capabilities -#[derive(Debug, Clone, Default)] -pub struct ProtocolInfo { - pub name: &'static str, - pub scheme: &'static str, - pub default_port: u16, - pub encrypted: bool, - pub bidirectional: bool, // can upload - pub streaming: bool, - pub anonymous: bool, -} - -/// Supported exotic protocols -pub const PROTOCOLS: &[ProtocolInfo] = &[ - ProtocolInfo { - name: "Usenet (NNTP)", - scheme: "nntp", - default_port: 119, - encrypted: false, - bidirectional: true, - streaming: false, - anonymous: false, - }, - ProtocolInfo { - name: "Usenet Secure (NNTPS)", - scheme: "nntps", - default_port: 563, - encrypted: true, - bidirectional: true, - streaming: false, - anonymous: false, - }, - ProtocolInfo { - name: "Gopher", - scheme: "gopher", - default_port: 70, - encrypted: false, - bidirectional: false, - streaming: false, - anonymous: true, - }, - ProtocolInfo { - name: "Gemini", - scheme: "gemini", - default_port: 1965, - encrypted: true, - bidirectional: false, - streaming: false, - anonymous: true, - }, - ProtocolInfo { - name: "RTSP (Streaming)", - scheme: "rtsp", - default_port: 554, - encrypted: false, - bidirectional: false, - streaming: true, - anonymous: false, - }, - ProtocolInfo { - name: "BitTorrent", - scheme: "magnet", - default_port: 6881, - encrypted: false, - bidirectional: true, - streaming: false, - anonymous: false, - }, - ProtocolInfo { - name: "DAT/Hypercore", - scheme: "dat", - default_port: 3282, - encrypted: true, - bidirectional: true, - streaming: true, - anonymous: false, - }, - ProtocolInfo { - name: "Freenet", - scheme: "freenet", - default_port: 8888, - encrypted: true, - bidirectional: true, - streaming: false, - anonymous: true, - }, - ProtocolInfo { - name: "I2P", - scheme: "i2p", - default_port: 7657, - encrypted: true, - bidirectional: true, - streaming: false, - anonymous: true, - }, - ProtocolInfo { - name: "Tor Onion", - scheme: "onion", - default_port: 9050, - encrypted: true, - bidirectional: true, - streaming: false, - anonymous: true, - }, - ProtocolInfo { - name: "MTP (Mobile)", - scheme: "mtp", - default_port: 0, - encrypted: false, - bidirectional: true, - streaming: false, - anonymous: false, - }, - ProtocolInfo { - name: "AFP (Apple)", - scheme: "afp", - default_port: 548, - encrypted: false, - bidirectional: true, - streaming: false, - anonymous: false, - }, - ProtocolInfo { - name: "DLNA/UPnP", - scheme: "dlna", - default_port: 1900, - encrypted: false, - bidirectional: false, - streaming: true, - anonymous: false, - }, - ProtocolInfo { - name: "Matrix", - scheme: "matrix", - default_port: 8448, - encrypted: true, - bidirectional: true, - streaming: false, - anonymous: false, - }, -]; - -/// Find protocol info by scheme -pub fn get_protocol(scheme: &str) -> Option<&'static ProtocolInfo> { - PROTOCOLS.iter().find(|p| p.scheme == scheme) -} - -/// List all supported protocol schemes -pub fn list_schemes() -> Vec<&'static str> { - PROTOCOLS.iter().map(|p| p.scheme).collect() -} - -/// Gopher client stub -pub mod gopher { - use super::*; - - /// Gopher item types - #[derive(Debug, Clone, Copy)] - pub enum ItemType { - TextFile, // 0 - Directory, // 1 - CsoServer, // 2 - Error, // 3 - BinHex, // 4 - DosBinary, // 5 - UuEncoded, // 6 - IndexSearch, // 7 - Telnet, // 8 - Binary, // 9 - Mirror, // + - Gif, // g - Image, // I - Html, // h - Info, // i - Sound, // s - } - - /// Gopher directory entry - #[derive(Debug, Clone)] - pub struct GopherEntry { - pub item_type: ItemType, - pub display: String, - pub selector: String, - pub host: String, - pub port: u16, - } - - /// Fetch a gopher URL - pub async fn fetch(_url: &str) -> CfkResult> { - // TODO: Implement gopher client - Err(CfkError::Unsupported("Gopher client not yet implemented".into())) - } - - /// Parse gopher directory listing - pub fn parse_directory(_data: &[u8]) -> Vec { - // TODO: Parse gopher menu format - Vec::new() - } -} - -/// Gemini client stub -pub mod gemini { - use super::*; - - /// Gemini response status - #[derive(Debug, Clone, Copy)] - pub enum Status { - Input = 10, - Success = 20, - Redirect = 30, - TemporaryFailure = 40, - PermanentFailure = 50, - ClientCertRequired = 60, - } - - /// Fetch a gemini URL - pub async fn fetch(_url: &str) -> CfkResult<(Status, String, Vec)> { - // TODO: Implement gemini client with TLS - Err(CfkError::Unsupported("Gemini client not yet implemented".into())) - } -} - -/// NNTP client stub -pub mod nntp { - use super::*; - - /// NNTP article - #[derive(Debug, Clone)] - pub struct Article { - pub message_id: String, - pub subject: String, - pub from: String, - pub date: String, - pub newsgroups: Vec, - pub body: String, - } - - /// Connect to NNTP server - pub async fn connect(_host: &str, _port: u16, _tls: bool) -> CfkResult<()> { - Err(CfkError::Unsupported("NNTP client not yet implemented".into())) - } - - /// List newsgroups - pub async fn list_groups() -> CfkResult> { - Err(CfkError::Unsupported("NNTP client not yet implemented".into())) - } -} diff --git a/czech-file-knife/cfk-providers/src/s3.rs b/czech-file-knife/cfk-providers/src/s3.rs deleted file mode 100644 index b634e168a..000000000 --- a/czech-file-knife/cfk-providers/src/s3.rs +++ /dev/null @@ -1,711 +0,0 @@ -//! S3-compatible storage backend -//! -//! Works with AWS S3, MinIO, Wasabi, DigitalOcean Spaces, Backblaze B2, -//! Cloudflare R2, and any S3-compatible object storage. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use reqwest::{header, Client, Method, StatusCode}; -use serde::Deserialize; -use std::collections::BTreeMap; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// S3 backend configuration -#[derive(Debug, Clone)] -pub struct S3Config { - /// S3 endpoint URL (e.g., "https://s3.amazonaws.com" or "https://minio.example.com") - pub endpoint: String, - /// Bucket name - pub bucket: String, - /// AWS region - pub region: String, - /// Access key ID - pub access_key_id: String, - /// Secret access key - pub secret_access_key: String, - /// Use path-style URLs (required for MinIO and some providers) - pub path_style: bool, -} - -impl S3Config { - /// Create AWS S3 configuration - pub fn aws(bucket: &str, region: &str, access_key: &str, secret_key: &str) -> Self { - Self { - endpoint: format!("https://s3.{}.amazonaws.com", region), - bucket: bucket.to_string(), - region: region.to_string(), - access_key_id: access_key.to_string(), - secret_access_key: secret_key.to_string(), - path_style: false, - } - } - - /// Create MinIO configuration - pub fn minio(endpoint: &str, bucket: &str, access_key: &str, secret_key: &str) -> Self { - Self { - endpoint: endpoint.to_string(), - bucket: bucket.to_string(), - region: "us-east-1".to_string(), - access_key_id: access_key.to_string(), - secret_access_key: secret_key.to_string(), - path_style: true, - } - } - - /// Create Cloudflare R2 configuration - pub fn r2(account_id: &str, bucket: &str, access_key: &str, secret_key: &str) -> Self { - Self { - endpoint: format!("https://{}.r2.cloudflarestorage.com", account_id), - bucket: bucket.to_string(), - region: "auto".to_string(), - access_key_id: access_key.to_string(), - secret_access_key: secret_key.to_string(), - path_style: true, - } - } - - /// Create Backblaze B2 configuration - pub fn b2(bucket: &str, region: &str, key_id: &str, app_key: &str) -> Self { - Self { - endpoint: format!("https://s3.{}.backblazeb2.com", region), - bucket: bucket.to_string(), - region: region.to_string(), - access_key_id: key_id.to_string(), - secret_access_key: app_key.to_string(), - path_style: false, - } - } - - /// Create DigitalOcean Spaces configuration - pub fn digitalocean(region: &str, space: &str, key: &str, secret: &str) -> Self { - Self { - endpoint: format!("https://{}.digitaloceanspaces.com", region), - bucket: space.to_string(), - region: region.to_string(), - access_key_id: key.to_string(), - secret_access_key: secret.to_string(), - path_style: false, - } - } - - /// Create Wasabi configuration - pub fn wasabi(bucket: &str, region: &str, access_key: &str, secret_key: &str) -> Self { - Self { - endpoint: format!("https://s3.{}.wasabisys.com", region), - bucket: bucket.to_string(), - region: region.to_string(), - access_key_id: access_key.to_string(), - secret_access_key: secret_key.to_string(), - path_style: false, - } - } -} - -/// S3 storage backend -pub struct S3Backend { - id: String, - config: Arc>, - http: Client, - capabilities: StorageCapabilities, -} - -impl S3Backend { - pub fn new(id: impl Into, config: S3Config) -> Self { - Self { - id: id.into(), - config: Arc::new(RwLock::new(config)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: false, // S3 doesn't support rename, need copy+delete - copy: true, - list: true, - search: false, - versioning: true, - sharing: true, // Presigned URLs - streaming: true, - resume: true, // Multipart upload - watch: false, - metadata: true, - thumbnails: false, - max_file_size: Some(5 * 1024 * 1024 * 1024 * 1024), // 5TB - }, - } - } - - /// Build URL for an object - async fn object_url(&self, key: &str) -> String { - let config = self.config.read().await; - - if config.path_style { - format!( - "{}/{}/{}", - config.endpoint.trim_end_matches('/'), - config.bucket, - key.trim_start_matches('/') - ) - } else { - // Virtual-hosted style - let endpoint = config.endpoint.replace("://", &format!("://{}.bucket.", config.bucket)); - format!("{}/{}", endpoint.trim_end_matches('/'), key.trim_start_matches('/')) - } - } - - /// Build URL for bucket operations - async fn bucket_url(&self) -> String { - let config = self.config.read().await; - - if config.path_style { - format!( - "{}/{}", - config.endpoint.trim_end_matches('/'), - config.bucket - ) - } else { - config.endpoint.replace("://", &format!("://{}.bucket.", config.bucket)) - } - } - - /// Sign request with AWS Signature Version 4 - async fn sign_request( - &self, - method: &Method, - url: &str, - headers: &mut BTreeMap, - payload_hash: &str, - ) -> CfkResult { - let config = self.config.read().await; - let now = Utc::now(); - let date_stamp = now.format("%Y%m%d").to_string(); - let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string(); - - headers.insert("x-amz-date".to_string(), amz_date.clone()); - headers.insert("x-amz-content-sha256".to_string(), payload_hash.to_string()); - - // Parse URL - let parsed = url::Url::parse(url).map_err(|e| CfkError::InvalidPath(e.to_string()))?; - let host = parsed.host_str().unwrap_or(""); - let path = parsed.path(); - let query = parsed.query().unwrap_or(""); - - headers.insert("host".to_string(), host.to_string()); - - // Create canonical request - let signed_headers: Vec<&str> = headers.keys().map(|s| s.as_str()).collect(); - let signed_headers_str = signed_headers.join(";"); - - let canonical_headers: String = headers - .iter() - .map(|(k, v)| format!("{}:{}\n", k.to_lowercase(), v.trim())) - .collect(); - - let canonical_request = format!( - "{}\n{}\n{}\n{}\n{}\n{}", - method.as_str(), - path, - query, - canonical_headers, - signed_headers_str, - payload_hash - ); - - let canonical_request_hash = sha256_hex(canonical_request.as_bytes()); - - // Create string to sign - let credential_scope = format!("{}/{}/s3/aws4_request", date_stamp, config.region); - let string_to_sign = format!( - "AWS4-HMAC-SHA256\n{}\n{}\n{}", - amz_date, credential_scope, canonical_request_hash - ); - - // Calculate signature - let k_date = hmac_sha256( - format!("AWS4{}", config.secret_access_key).as_bytes(), - date_stamp.as_bytes(), - ); - let k_region = hmac_sha256(&k_date, config.region.as_bytes()); - let k_service = hmac_sha256(&k_region, b"s3"); - let k_signing = hmac_sha256(&k_service, b"aws4_request"); - let signature = hex::encode(hmac_sha256(&k_signing, string_to_sign.as_bytes())); - - // Build authorization header - let authorization = format!( - "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", - config.access_key_id, credential_scope, signed_headers_str, signature - ); - - Ok(authorization) - } - - /// Make signed request - async fn request( - &self, - method: Method, - key: &str, - body: Option, - ) -> CfkResult { - let url = if key.is_empty() { - self.bucket_url().await - } else { - self.object_url(key).await - }; - - let payload_hash = if let Some(ref data) = body { - sha256_hex(data) - } else { - sha256_hex(b"") - }; - - let mut headers = BTreeMap::new(); - let auth = self.sign_request(&method, &url, &mut headers, &payload_hash).await?; - - let mut request = self.http.request(method, &url); - - for (k, v) in &headers { - request = request.header(k, v); - } - request = request.header(header::AUTHORIZATION, auth); - - if let Some(data) = body { - request = request.body(data.to_vec()); - } - - request - .send() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - /// List objects with prefix - async fn list_objects( - &self, - prefix: &str, - delimiter: Option<&str>, - ) -> CfkResult { - let config = self.config.read().await; - let mut url = format!( - "{}/{}?list-type=2", - config.endpoint.trim_end_matches('/'), - config.bucket - ); - - if !prefix.is_empty() { - url.push_str(&format!("&prefix={}", urlencoding::encode(prefix))); - } - if let Some(d) = delimiter { - url.push_str(&format!("&delimiter={}", urlencoding::encode(d))); - } - - drop(config); - - let payload_hash = sha256_hex(b""); - let mut headers = BTreeMap::new(); - let auth = self.sign_request(&Method::GET, &url, &mut headers, &payload_hash).await?; - - let mut request = self.http.get(&url); - for (k, v) in &headers { - request = request.header(k, v); - } - request = request.header(header::AUTHORIZATION, auth); - - let response = request - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let text = response.text().await.map_err(|e| CfkError::Network(e.to_string()))?; - parse_list_objects_v2(&text) - } - - /// Convert VirtualPath to S3 key - fn to_key(&self, path: &VirtualPath) -> String { - path.segments.join("/") - } -} - -/// S3 object metadata -#[derive(Debug, Clone, Default)] -struct S3Object { - key: String, - size: u64, - last_modified: Option>, - etag: Option, - storage_class: Option, -} - -/// Common prefix (directory) in listing -#[derive(Debug, Clone)] -struct CommonPrefix { - prefix: String, -} - -/// List objects result -#[derive(Debug, Clone, Default)] -struct ListObjectsResult { - objects: Vec, - common_prefixes: Vec, - is_truncated: bool, - continuation_token: Option, -} - -/// Parse ListObjectsV2 XML response -fn parse_list_objects_v2(xml: &str) -> CfkResult { - let mut result = ListObjectsResult::default(); - let mut in_contents = false; - let mut current_object = S3Object::default(); - - for line in xml.lines() { - let line = line.trim(); - - if line.contains("") { - in_contents = true; - current_object = S3Object::default(); - } else if line.contains("") { - in_contents = false; - result.objects.push(current_object.clone()); - } else if in_contents { - if let Some(key) = extract_xml_value(line, "Key") { - current_object.key = key; - } - if let Some(size) = extract_xml_value(line, "Size") { - current_object.size = size.parse().unwrap_or(0); - } - if let Some(modified) = extract_xml_value(line, "LastModified") { - current_object.last_modified = DateTime::parse_from_rfc3339(&modified) - .ok() - .map(|dt| dt.with_timezone(&Utc)); - } - if let Some(etag) = extract_xml_value(line, "ETag") { - current_object.etag = Some(etag.trim_matches('"').to_string()); - } - if let Some(class) = extract_xml_value(line, "StorageClass") { - current_object.storage_class = Some(class); - } - } - - if let Some(prefix) = extract_xml_value(line, "Prefix") { - if line.contains("") || xml.contains("") { - result.common_prefixes.push(CommonPrefix { prefix }); - } - } - - if let Some(truncated) = extract_xml_value(line, "IsTruncated") { - result.is_truncated = truncated == "true"; - } - - if let Some(token) = extract_xml_value(line, "NextContinuationToken") { - result.continuation_token = Some(token); - } - } - - Ok(result) -} - -/// Extract value from XML element -fn extract_xml_value(line: &str, tag: &str) -> Option { - let start_tag = format!("<{}>", tag); - let end_tag = format!("", tag); - - if let Some(start) = line.find(&start_tag) { - let content_start = start + start_tag.len(); - if let Some(end) = line[content_start..].find(&end_tag) { - return Some(line[content_start..content_start + end].to_string()); - } - } - None -} - -/// SHA-256 hash as hex string -fn sha256_hex(data: &[u8]) -> String { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(data); - hex::encode(hasher.finalize()) -} - -/// HMAC-SHA256 -fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec { - use hmac::{Hmac, Mac}; - use sha2::Sha256; - - type HmacSha256 = Hmac; - let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size"); - mac.update(data); - mac.finalize().into_bytes().to_vec() -} - -#[async_trait] -impl StorageBackend for S3Backend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "S3" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.list_objects("", Some("/")).await.is_ok() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let key = self.to_key(path); - - if key.is_empty() { - // Root - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - - // HEAD request - let response = self.request(Method::HEAD, &key, None).await?; - - if response.status() == StatusCode::NOT_FOUND { - // Check if it's a directory (prefix) - let prefix = format!("{}/", key); - let list = self.list_objects(&prefix, Some("/")).await?; - if !list.objects.is_empty() || !list.common_prefixes.is_empty() { - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - return Err(CfkError::NotFound(path.to_string())); - } - - if !response.status().is_success() { - let status = response.status(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}", status), - }); - } - - let headers = response.headers(); - let mut metadata = Metadata::default(); - - if let Some(len) = headers.get(header::CONTENT_LENGTH) { - metadata.size = len.to_str().ok().and_then(|s| s.parse().ok()); - } - - if let Some(modified) = headers.get(header::LAST_MODIFIED) { - if let Ok(s) = modified.to_str() { - metadata.modified = DateTime::parse_from_rfc2822(s) - .ok() - .map(|dt| dt.with_timezone(&Utc)); - } - } - - if let Some(etag) = headers.get(header::ETAG) { - metadata.checksum = etag.to_str().ok().map(|s| s.trim_matches('"').to_string()); - } - - if let Some(ct) = headers.get(header::CONTENT_TYPE) { - metadata.mime_type = ct.to_str().ok().map(String::from); - } - - Ok(Entry { - path: path.clone(), - kind: EntryKind::File, - metadata, - }) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let mut prefix = self.to_key(path); - if !prefix.is_empty() && !prefix.ends_with('/') { - prefix.push('/'); - } - - let result = self.list_objects(&prefix, Some("/")).await?; - - let mut entries = Vec::new(); - - // Add objects - for obj in result.objects { - let key = obj.key.trim_start_matches(&prefix); - if key.is_empty() || key == "/" { - continue; - } - - let mut metadata = Metadata::default(); - metadata.size = Some(obj.size); - metadata.modified = obj.last_modified; - metadata.checksum = obj.etag; - - entries.push(Entry { - path: VirtualPath::new(&self.id, &obj.key), - kind: EntryKind::File, - metadata, - }); - } - - // Add directories (common prefixes) - for cp in result.common_prefixes { - let dir_name = cp.prefix.trim_end_matches('/'); - entries.push(Entry { - path: VirtualPath::new(&self.id, dir_name), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - - Ok(entries) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let key = self.to_key(path); - let response = self.request(Method::GET, &key, None).await?; - - if !response.status().is_success() { - let status = response.status(); - if status == StatusCode::NOT_FOUND { - return Err(CfkError::NotFound(path.to_string())); - } - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let key = self.to_key(path); - let response = self.request(Method::PUT, &key, Some(data)).await?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(path).await - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let key = self.to_key(path); - let response = self.request(Method::DELETE, &key, None).await?; - - if !response.status().is_success() && response.status() != StatusCode::NO_CONTENT { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - // S3 doesn't have real directories, create a zero-byte object with trailing slash - let mut key = self.to_key(path); - if !key.ends_with('/') { - key.push('/'); - } - - let response = self.request(Method::PUT, &key, Some(Bytes::new())).await?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_key = self.to_key(from); - let to_key = self.to_key(to); - let config = self.config.read().await; - - let copy_source = format!("{}/{}", config.bucket, from_key); - drop(config); - - // Build copy request with x-amz-copy-source header - let url = self.object_url(&to_key).await; - let payload_hash = sha256_hex(b""); - - let mut headers = BTreeMap::new(); - headers.insert("x-amz-copy-source".to_string(), copy_source); - - let auth = self.sign_request(&Method::PUT, &url, &mut headers, &payload_hash).await?; - - let mut request = self.http.put(&url); - for (k, v) in &headers { - request = request.header(k, v); - } - request = request.header(header::AUTHORIZATION, auth); - - let response = request - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "s3".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(to).await - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - // S3 doesn't support rename, use copy + delete - let entry = self.copy(from, to).await?; - self.delete(from).await?; - Ok(entry) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // S3 doesn't have quota concept, return 0 - Ok((0, 0)) - } -} diff --git a/czech-file-knife/cfk-providers/src/sftp.rs b/czech-file-knife/cfk-providers/src/sftp.rs deleted file mode 100644 index a99c43b9d..000000000 --- a/czech-file-knife/cfk-providers/src/sftp.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! SFTP storage backend -//! -//! SSH File Transfer Protocol implementation. -//! Supports password, key-based, and agent authentication. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use std::path::PathBuf; - -/// SFTP authentication method -#[derive(Debug, Clone)] -pub enum SftpAuth { - /// Password authentication - Password { username: String, password: String }, - /// Private key authentication - PrivateKey { - username: String, - private_key_path: PathBuf, - passphrase: Option, - }, - /// SSH agent authentication - Agent { username: String }, -} - -/// SFTP backend configuration -#[derive(Debug, Clone)] -pub struct SftpConfig { - /// Host address - pub host: String, - /// Port (default: 22) - pub port: u16, - /// Authentication method - pub auth: SftpAuth, - /// Known hosts file path - pub known_hosts: Option, - /// Skip host key verification (insecure!) - pub skip_host_key_check: bool, - /// Remote base path - pub base_path: String, -} - -impl Default for SftpConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 22, - auth: SftpAuth::Agent { - username: whoami::username(), - }, - known_hosts: None, - skip_host_key_check: false, - base_path: "/".to_string(), - } - } -} - -/// SFTP storage backend -/// -/// Note: This is a stub implementation. Full implementation would use -/// the `ssh2` or `russh` crate for SSH/SFTP protocol support. -pub struct SftpBackend { - id: String, - config: SftpConfig, - capabilities: StorageCapabilities, - // In a full implementation: - // session: Option, - // sftp: Option, -} - -impl SftpBackend { - pub fn new(id: impl Into, config: SftpConfig) -> Self { - Self { - id: id.into(), - config, - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: false, // SFTP doesn't have native copy - list: true, - search: false, - versioning: false, - sharing: false, - streaming: true, - resume: true, // With SEEK - watch: false, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - } - } - - /// Create from SSH URL: sftp://user@host:port/path - pub fn from_url(id: impl Into, url: &str) -> CfkResult { - let parsed = url::Url::parse(url) - .map_err(|e| CfkError::InvalidPath(format!("Invalid URL: {}", e)))?; - - if parsed.scheme() != "sftp" { - return Err(CfkError::InvalidPath("URL scheme must be sftp".into())); - } - - let host = parsed - .host_str() - .ok_or_else(|| CfkError::InvalidPath("Missing host".into()))? - .to_string(); - - let port = parsed.port().unwrap_or(22); - let username = if parsed.username().is_empty() { - whoami::username() - } else { - parsed.username().to_string() - }; - - let base_path = if parsed.path().is_empty() { - "/".to_string() - } else { - parsed.path().to_string() - }; - - let auth = if let Some(password) = parsed.password() { - SftpAuth::Password { - username, - password: password.to_string(), - } - } else { - SftpAuth::Agent { username } - }; - - Ok(Self::new( - id, - SftpConfig { - host, - port, - auth, - base_path, - ..Default::default() - }, - )) - } - - /// Convert VirtualPath to remote path - fn to_remote_path(&self, path: &VirtualPath) -> String { - let base = self.config.base_path.trim_end_matches('/'); - if path.segments.is_empty() { - base.to_string() - } else { - format!("{}/{}", base, path.segments.join("/")) - } - } - - /// Connect to SFTP server - pub async fn connect(&self) -> CfkResult<()> { - // In a full implementation, this would: - // 1. Create TCP connection - // 2. Perform SSH handshake - // 3. Authenticate - // 4. Initialize SFTP subsystem - - Err(CfkError::Unsupported( - "SFTP backend requires ssh2 or russh crate. Stub implementation.".into(), - )) - } -} - -#[async_trait] -impl StorageBackend for SftpBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "SFTP" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - // Would check SSH connection - false - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let _remote_path = self.to_remote_path(path); - - // Would use SFTP stat() call - // let attrs = sftp.stat(&remote_path)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let _remote_path = self.to_remote_path(path); - - // Would use SFTP readdir() call - // let entries = sftp.readdir(&remote_path)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let _remote_path = self.to_remote_path(path); - - // Would open file and read: - // let mut file = sftp.open(&remote_path)?; - // let mut data = Vec::new(); - // file.read_to_end(&mut data)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn write_file(&self, path: &VirtualPath, _data: Bytes) -> CfkResult { - let _remote_path = self.to_remote_path(path); - - // Would create/open file and write: - // let mut file = sftp.create(&remote_path)?; - // file.write_all(&data)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let _remote_path = self.to_remote_path(path); - - // Would use SFTP unlink() or rmdir(): - // sftp.unlink(&remote_path)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let _remote_path = self.to_remote_path(path); - - // Would use SFTP mkdir(): - // sftp.mkdir(&remote_path, 0o755)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - // SFTP doesn't support server-side copy - // Would need to read + write - Err(CfkError::Unsupported( - "SFTP doesn't support native copy".into(), - )) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let _from_path = self.to_remote_path(from); - let _to_path = self.to_remote_path(to); - - // Would use SFTP rename(): - // sftp.rename(&from_path, &to_path, None)?; - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // SFTP has statvfs extension (OpenSSH) - // Would use sftp.statvfs() - - Err(CfkError::Unsupported("SFTP stub - use ssh2 crate".into())) - } -} - -/// Helper to get username -mod whoami { - pub fn username() -> String { - std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "nobody".to_string()) - } -} - -/// SFTP file attributes (mirrors ssh2::FileStat) -#[derive(Debug, Clone, Default)] -pub struct FileAttributes { - pub size: Option, - pub uid: Option, - pub gid: Option, - pub permissions: Option, - pub atime: Option, - pub mtime: Option, -} - -impl FileAttributes { - pub fn is_dir(&self) -> bool { - self.permissions - .map(|p| (p & 0o40000) != 0) - .unwrap_or(false) - } - - pub fn is_symlink(&self) -> bool { - self.permissions - .map(|p| (p & 0o120000) == 0o120000) - .unwrap_or(false) - } - - pub fn is_file(&self) -> bool { - self.permissions - .map(|p| (p & 0o100000) != 0) - .unwrap_or(false) - } - - pub fn to_metadata(&self) -> Metadata { - let mut meta = Metadata::default(); - meta.size = self.size; - meta.permissions = self.permissions; - meta.uid = self.uid; - meta.gid = self.gid; - - if let Some(mtime) = self.mtime { - meta.modified = chrono::DateTime::from_timestamp(mtime as i64, 0); - } - - meta - } -} diff --git a/czech-file-knife/cfk-providers/src/smb.rs b/czech-file-knife/cfk-providers/src/smb.rs deleted file mode 100644 index fd70bcee3..000000000 --- a/czech-file-knife/cfk-providers/src/smb.rs +++ /dev/null @@ -1,494 +0,0 @@ -//! SMB/CIFS storage backend -//! -//! Server Message Block / Common Internet File System protocol. -//! Compatible with Windows shares, Samba, and macOS file sharing. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use std::path::PathBuf; - -/// SMB protocol version -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SmbVersion { - /// SMB 1.0 (legacy, insecure) - Smb1, - /// SMB 2.0 - Smb2, - /// SMB 2.1 - Smb21, - /// SMB 3.0 - Smb3, - /// SMB 3.0.2 - Smb302, - /// SMB 3.1.1 - Smb311, -} - -impl Default for SmbVersion { - fn default() -> Self { - Self::Smb3 // Secure default - } -} - -/// SMB authentication -#[derive(Debug, Clone)] -pub enum SmbAuth { - /// Anonymous/Guest access - Anonymous, - /// NTLM authentication - Ntlm { username: String, password: String, domain: Option }, - /// Kerberos authentication - Kerberos { principal: String }, -} - -impl Default for SmbAuth { - fn default() -> Self { - Self::Anonymous - } -} - -/// SMB backend configuration -#[derive(Debug, Clone)] -pub struct SmbConfig { - /// Server hostname or IP - pub server: String, - /// Share name - pub share: String, - /// SMB protocol version - pub version: SmbVersion, - /// Authentication - pub auth: SmbAuth, - /// Port (default: 445, legacy: 139) - pub port: u16, - /// Encrypt traffic (SMB 3.0+) - pub encryption: bool, - /// Sign messages - pub signing: bool, -} - -impl Default for SmbConfig { - fn default() -> Self { - Self { - server: "localhost".to_string(), - share: "share".to_string(), - version: SmbVersion::default(), - auth: SmbAuth::default(), - port: 445, - encryption: true, - signing: true, - } - } -} - -/// SMB tree connection ID -#[derive(Debug, Clone, Copy, Default)] -struct TreeId(u32); - -/// SMB session ID -#[derive(Debug, Clone, Copy, Default)] -struct SessionId(u64); - -/// SMB file ID -#[derive(Debug, Clone, Copy, Default)] -struct FileId { - persistent: u64, - volatile: u64, -} - -/// SMB storage backend -/// -/// Note: This is a stub implementation. Full implementation would require -/// the SMB protocol which is complex. Consider using `pavao` or `smb` crate, -/// or system mount. -pub struct SmbBackend { - id: String, - config: SmbConfig, - capabilities: StorageCapabilities, - session: Option, - tree_id: Option, -} - -impl SmbBackend { - pub fn new(id: impl Into, config: SmbConfig) -> Self { - let mut caps = StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, // SMB2+ has server-side copy - list: true, - search: true, // SMB has FIND - versioning: false, - sharing: true, // Windows ACLs - streaming: true, - resume: true, - watch: true, // Change notifications - metadata: true, - thumbnails: false, - max_file_size: None, - }; - - // Adjust capabilities based on version - if config.version == SmbVersion::Smb1 { - caps.copy = false; // SMB1 doesn't have server-side copy - caps.watch = false; - } - - Self { - id: id.into(), - config, - capabilities: caps, - session: None, - tree_id: None, - } - } - - /// Create from SMB URL: smb://user:pass@server/share - pub fn from_url(id: impl Into, url: &str) -> CfkResult { - let parsed = url::Url::parse(url) - .map_err(|e| CfkError::InvalidPath(format!("Invalid URL: {}", e)))?; - - if parsed.scheme() != "smb" { - return Err(CfkError::InvalidPath("URL scheme must be smb".into())); - } - - let server = parsed - .host_str() - .ok_or_else(|| CfkError::InvalidPath("Missing server".into()))? - .to_string(); - - let share = parsed - .path() - .trim_start_matches('/') - .split('/') - .next() - .unwrap_or("share") - .to_string(); - - let port = parsed.port().unwrap_or(445); - - let auth = if !parsed.username().is_empty() { - SmbAuth::Ntlm { - username: parsed.username().to_string(), - password: parsed.password().unwrap_or("").to_string(), - domain: None, - } - } else { - SmbAuth::Anonymous - }; - - Ok(Self::new( - id, - SmbConfig { - server, - share, - port, - auth, - ..Default::default() - }, - )) - } - - /// Connect to SMB server - pub async fn connect(&mut self) -> CfkResult<()> { - // SMB2/3 connection sequence: - // 1. TCP connect to port 445 - // 2. NEGOTIATE (select protocol version) - // 3. SESSION_SETUP (authenticate) - // 4. TREE_CONNECT (connect to share) - - Err(CfkError::Unsupported( - "SMB backend is a stub. Use system mount, pavao, or smb crate.".into(), - )) - } - - /// Disconnect from SMB server - pub async fn disconnect(&mut self) -> CfkResult<()> { - // 1. TREE_DISCONNECT - // 2. LOGOFF - // 3. Close TCP connection - - self.tree_id = None; - self.session = None; - Ok(()) - } - - /// Convert VirtualPath to SMB path (backslashes) - fn to_smb_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - "\\".to_string() - } else { - format!("\\{}", path.segments.join("\\")) - } - } -} - -#[async_trait] -impl StorageBackend for SmbBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - match self.config.version { - SmbVersion::Smb1 => "SMB1/CIFS", - SmbVersion::Smb2 => "SMB2", - SmbVersion::Smb21 => "SMB2.1", - SmbVersion::Smb3 => "SMB3", - SmbVersion::Smb302 => "SMB3.0.2", - SmbVersion::Smb311 => "SMB3.1.1", - } - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.session.is_some() && self.tree_id.is_some() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let _smb_path = self.to_smb_path(path); - // Would use QUERY_INFO with FileAllInformation class - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let _smb_path = self.to_smb_path(path); - // Would use QUERY_DIRECTORY (SMB2) or FIND_FIRST2/FIND_NEXT2 (SMB1) - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let _smb_path = self.to_smb_path(path); - // Would use CREATE (open) + READ + CLOSE - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn write_file(&self, path: &VirtualPath, _data: Bytes) -> CfkResult { - let _smb_path = self.to_smb_path(path); - // Would use CREATE + WRITE + CLOSE - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let _smb_path = self.to_smb_path(path); - // Would use CREATE with DELETE_ON_CLOSE or SET_INFO with FileDispositionInfo - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let _smb_path = self.to_smb_path(path); - // Would use CREATE with FILE_DIRECTORY_FILE - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - if self.config.version == SmbVersion::Smb1 { - return Err(CfkError::Unsupported("SMB1 doesn't support server-side copy".into())); - } - - let _from_path = self.to_smb_path(from); - let _to_path = self.to_smb_path(to); - // Would use IOCTL with FSCTL_SRV_COPYCHUNK - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let _from_path = self.to_smb_path(from); - let _to_path = self.to_smb_path(to); - // Would use SET_INFO with FileRenameInformation - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // Would use QUERY_INFO with FileFsFullSizeInformation - - Err(CfkError::Unsupported("SMB stub - use system mount".into())) - } -} - -/// SMB file attributes -#[derive(Debug, Clone, Copy, Default)] -pub struct SmbFileAttributes(u32); - -impl SmbFileAttributes { - pub const READONLY: u32 = 0x0001; - pub const HIDDEN: u32 = 0x0002; - pub const SYSTEM: u32 = 0x0004; - pub const DIRECTORY: u32 = 0x0010; - pub const ARCHIVE: u32 = 0x0020; - pub const NORMAL: u32 = 0x0080; - pub const TEMPORARY: u32 = 0x0100; - pub const SPARSE: u32 = 0x0200; - pub const REPARSE_POINT: u32 = 0x0400; - pub const COMPRESSED: u32 = 0x0800; - pub const ENCRYPTED: u32 = 0x4000; - - pub fn is_directory(&self) -> bool { - self.0 & Self::DIRECTORY != 0 - } - - pub fn is_hidden(&self) -> bool { - self.0 & Self::HIDDEN != 0 - } - - pub fn is_readonly(&self) -> bool { - self.0 & Self::READONLY != 0 - } - - pub fn is_symlink(&self) -> bool { - self.0 & Self::REPARSE_POINT != 0 - } -} - -/// SMB file information -#[derive(Debug, Clone, Default)] -pub struct SmbFileInfo { - pub creation_time: u64, - pub last_access_time: u64, - pub last_write_time: u64, - pub change_time: u64, - pub attributes: SmbFileAttributes, - pub allocation_size: u64, - pub end_of_file: u64, - pub file_id: u64, -} - -impl SmbFileInfo { - /// Convert Windows FILETIME to Unix timestamp - fn filetime_to_unix(ft: u64) -> Option { - // FILETIME is 100-nanosecond intervals since Jan 1, 1601 - // Unix epoch is Jan 1, 1970 - const FILETIME_UNIX_DIFF: u64 = 116444736000000000; - if ft > FILETIME_UNIX_DIFF { - Some(((ft - FILETIME_UNIX_DIFF) / 10000000) as i64) - } else { - None - } - } - - pub fn to_entry(&self, backend_id: &str, path: &str) -> Entry { - let kind = if self.attributes.is_directory() { - EntryKind::Directory - } else if self.attributes.is_symlink() { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = Some(self.end_of_file); - - if let Some(ts) = Self::filetime_to_unix(self.last_write_time) { - metadata.modified = chrono::DateTime::from_timestamp(ts, 0); - } - if let Some(ts) = Self::filetime_to_unix(self.creation_time) { - metadata.created = chrono::DateTime::from_timestamp(ts, 0); - } - - metadata.custom.insert( - "readonly".to_string(), - self.attributes.is_readonly().to_string(), - ); - metadata.custom.insert( - "hidden".to_string(), - self.attributes.is_hidden().to_string(), - ); - - Entry { - path: VirtualPath::new(backend_id, path), - kind, - metadata, - } - } -} - -/// Helper to use system mount -impl SmbBackend { - /// Mount using system mount.cifs (Linux) or mount_smbfs (macOS) - pub fn mount_system(&self, mount_point: &PathBuf) -> CfkResult<()> { - use std::process::Command; - - let source = format!("//{}/{}", self.config.server, self.config.share); - - #[cfg(target_os = "linux")] - { - let (username, password) = match &self.config.auth { - SmbAuth::Anonymous => ("guest".to_string(), String::new()), - SmbAuth::Ntlm { username, password, .. } => (username.clone(), password.clone()), - SmbAuth::Kerberos { .. } => { - return Err(CfkError::Unsupported( - "Kerberos mount requires system configuration".into(), - )) - } - }; - - let options = format!( - "username={},password={},vers={}", - username, - password, - match self.config.version { - SmbVersion::Smb1 => "1.0", - SmbVersion::Smb2 => "2.0", - SmbVersion::Smb21 => "2.1", - SmbVersion::Smb3 | SmbVersion::Smb302 | SmbVersion::Smb311 => "3.0", - } - ); - - let status = Command::new("mount") - .args([ - "-t", "cifs", - "-o", &options, - &source, - mount_point.to_str().unwrap_or("/mnt"), - ]) - .status() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !status.success() { - return Err(CfkError::ProviderApi { - provider: "smb".into(), - message: "mount.cifs failed".into(), - }); - } - } - - #[cfg(target_os = "macos")] - { - let status = Command::new("mount_smbfs") - .args([&source, mount_point.to_str().unwrap_or("/mnt")]) - .status() - .map_err(|e| CfkError::Io(e.to_string()))?; - - if !status.success() { - return Err(CfkError::ProviderApi { - provider: "smb".into(), - message: "mount_smbfs failed".into(), - }); - } - } - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - return Err(CfkError::Unsupported( - "System SMB mount not supported on this platform".into(), - )); - } - - Ok(()) - } -} diff --git a/czech-file-knife/cfk-providers/src/syncthing.rs b/czech-file-knife/cfk-providers/src/syncthing.rs deleted file mode 100644 index 971da21cf..000000000 --- a/czech-file-knife/cfk-providers/src/syncthing.rs +++ /dev/null @@ -1,531 +0,0 @@ -//! Syncthing storage backend -//! -//! Connects to Syncthing's REST API to expose synced folders. -//! Note: Syncthing folders are local, this backend provides folder discovery and sync status. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Syncthing connection configuration -#[derive(Debug, Clone)] -pub struct SyncthingConfig { - pub api_url: String, - pub api_key: String, -} - -/// Syncthing folder information -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FolderConfig { - pub id: String, - pub label: String, - pub path: String, - #[serde(rename = "type")] - pub folder_type: String, - pub paused: bool, -} - -/// Syncthing folder status -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FolderStatus { - pub state: String, - pub local_files: u64, - pub local_bytes: u64, - pub global_files: u64, - pub global_bytes: u64, - pub need_files: u64, - pub need_bytes: u64, -} - -/// Syncthing storage backend -pub struct SyncthingBackend { - id: String, - http: Client, - config: Arc>, - capabilities: StorageCapabilities, - /// Cache of folder configs - folders: Arc>>, -} - -impl SyncthingBackend { - pub fn new(id: impl Into, config: SyncthingConfig) -> Self { - Self { - id: id.into(), - http: Client::new(), - config: Arc::new(RwLock::new(config)), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: false, - versioning: true, - sharing: false, - streaming: true, - resume: true, - watch: true, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - folders: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Make API GET request - async fn api_get Deserialize<'de>>(&self, endpoint: &str) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/rest/{}", config.api_url, endpoint); - - let response = self - .http - .get(&url) - .header("X-API-Key", &config.api_key) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - return Err(CfkError::ProviderApi { - provider: "syncthing".into(), - message: response.text().await.unwrap_or_default(), - }); - } - - response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string())) - } - - /// Make API POST request - async fn api_post Deserialize<'de>>( - &self, - endpoint: &str, - body: impl Serialize, - ) -> CfkResult { - let config = self.config.read().await; - let url = format!("{}/rest/{}", config.api_url, endpoint); - - let response = self - .http - .post(&url) - .header("X-API-Key", &config.api_key) - .json(&body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - return Err(CfkError::ProviderApi { - provider: "syncthing".into(), - message: response.text().await.unwrap_or_default(), - }); - } - - response - .json() - .await - .map_err(|e| CfkError::Serialization(e.to_string())) - } - - /// Refresh folder list - pub async fn refresh_folders(&self) -> CfkResult<()> { - #[derive(Deserialize)] - struct ConfigResponse { - folders: Vec, - } - - let config: ConfigResponse = self.api_get("config").await?; - - let mut folders = self.folders.write().await; - folders.clear(); - for folder in config.folders { - folders.insert(folder.id.clone(), folder); - } - - Ok(()) - } - - /// Get folder status - pub async fn get_folder_status(&self, folder_id: &str) -> CfkResult { - self.api_get(&format!("db/status?folder={}", folder_id)) - .await - } - - /// Trigger rescan of a folder - pub async fn rescan_folder(&self, folder_id: &str) -> CfkResult<()> { - let config = self.config.read().await; - let url = format!( - "{}/rest/db/scan?folder={}", - config.api_url, folder_id - ); - - let response = self - .http - .post(&url) - .header("X-API-Key", &config.api_key) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - return Err(CfkError::ProviderApi { - provider: "syncthing".into(), - message: response.text().await.unwrap_or_default(), - }); - } - - Ok(()) - } - - /// Get local path for a virtual path - fn get_local_path(&self, folder_id: &str, subpath: &str) -> CfkResult { - // This would need the folder cache to be populated - // For now, return an error indicating the need for local backend - Err(CfkError::Unsupported(format!( - "Use LocalBackend for actual file operations on folder {} subpath {}", - folder_id, subpath - ))) - } - - /// Parse path into folder ID and subpath - fn parse_path(&self, path: &VirtualPath) -> (Option, String) { - if path.segments.is_empty() { - (None, String::new()) - } else { - let folder_id = path.segments[0].clone(); - let subpath = if path.segments.len() > 1 { - path.segments[1..].join("/") - } else { - String::new() - }; - (Some(folder_id), subpath) - } - } -} - -#[async_trait] -impl StorageBackend for SyncthingBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "Syncthing" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - #[derive(Deserialize)] - struct SystemStatus { - #[serde(rename = "myID")] - _my_id: String, - } - - self.api_get::("system/status").await.is_ok() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() { - // Root - return as directory - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::default(), - }); - } - - let folder_id = folder_id.unwrap(); - - if subpath.is_empty() { - // Folder root - let status = self.get_folder_status(&folder_id).await?; - - let mut metadata = Metadata::default(); - metadata.size = Some(status.local_bytes); - - return Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata, - }); - } - - // For subpaths, delegate to local backend - Err(CfkError::Unsupported( - "Use LocalBackend for file metadata in Syncthing folders".into(), - )) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() { - // List all synced folders - self.refresh_folders().await?; - - let folders = self.folders.read().await; - let entries: Vec = folders - .values() - .map(|f| { - let mut metadata = Metadata::default(); - // Use label as display name in custom metadata - metadata.custom - .insert("label".to_string(), f.label.clone()); - metadata - .custom - .insert("type".to_string(), f.folder_type.clone()); - metadata.custom.insert( - "paused".to_string(), - if f.paused { "true" } else { "false" }.to_string(), - ); - - Entry { - path: VirtualPath::new(&self.id, &f.id), - kind: EntryKind::Directory, - metadata, - } - }) - .collect(); - - return Ok(entries); - } - - if !subpath.is_empty() { - return Err(CfkError::Unsupported( - "Use LocalBackend to list files in Syncthing folders".into(), - )); - } - - // For folder contents, we need to use the database browse API - #[derive(Deserialize)] - struct BrowseEntry { - name: String, - #[serde(rename = "type")] - entry_type: String, - size: Option, - #[serde(rename = "modTime")] - mod_time: Option, - } - - let folder_id = folder_id.unwrap(); - let entries: Vec = self - .api_get(&format!("db/browse?folder={}&levels=1", folder_id)) - .await?; - - let base_path = path.segments.join("/"); - - Ok(entries - .iter() - .map(|e| { - let path_str = if base_path.is_empty() { - e.name.clone() - } else { - format!("{}/{}", base_path, e.name) - }; - - let kind = if e.entry_type == "d" { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = e.size; - - Entry { - path: VirtualPath::new(&self.id, &path_str), - kind, - metadata, - } - }) - .collect()) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() || subpath.is_empty() { - return Err(CfkError::InvalidPath("Cannot read folder as file".into())); - } - - // Syncthing doesn't provide file content via API - // Files must be accessed through local filesystem - Err(CfkError::Unsupported( - "Use LocalBackend to read files in Syncthing folders".into(), - )) - } - - async fn write_file(&self, path: &VirtualPath, _data: Bytes) -> CfkResult { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() || subpath.is_empty() { - return Err(CfkError::InvalidPath("Cannot write to folder".into())); - } - - Err(CfkError::Unsupported( - "Use LocalBackend to write files in Syncthing folders, then trigger rescan".into(), - )) - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() { - return Err(CfkError::InvalidPath("Cannot delete root".into())); - } - - if subpath.is_empty() { - return Err(CfkError::Unsupported( - "Cannot delete Syncthing folder via this API".into(), - )); - } - - Err(CfkError::Unsupported( - "Use LocalBackend to delete files in Syncthing folders".into(), - )) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let (folder_id, subpath) = self.parse_path(path); - - if folder_id.is_none() { - return Err(CfkError::Unsupported( - "Create new Syncthing folder via Syncthing UI".into(), - )); - } - - if subpath.is_empty() { - return Err(CfkError::AlreadyExists(path.to_string())); - } - - Err(CfkError::Unsupported( - "Use LocalBackend to create directories in Syncthing folders".into(), - )) - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - Err(CfkError::Unsupported( - "Use LocalBackend to copy files in Syncthing folders".into(), - )) - } - - async fn rename(&self, _from: &VirtualPath, _to: &VirtualPath) -> CfkResult { - Err(CfkError::Unsupported( - "Use LocalBackend to rename files in Syncthing folders".into(), - )) - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // Sum up space from all folders - self.refresh_folders().await?; - - let folders = self.folders.read().await; - let mut total_bytes = 0u64; - - for folder in folders.values() { - if let Ok(status) = self.get_folder_status(&folder.id).await { - total_bytes += status.local_bytes; - } - } - - // Syncthing doesn't track quota, return local bytes as both - Ok((total_bytes, total_bytes)) - } -} - -/// Syncthing device information -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeviceConfig { - pub device_id: String, - pub name: String, - pub addresses: Vec, - pub paused: bool, -} - -/// Extension methods for Syncthing-specific operations -impl SyncthingBackend { - /// List connected devices - pub async fn list_devices(&self) -> CfkResult> { - #[derive(Deserialize)] - struct ConfigResponse { - devices: Vec, - } - - let config: ConfigResponse = self.api_get("config").await?; - Ok(config.devices) - } - - /// Get system connections - pub async fn get_connections(&self) -> CfkResult> { - #[derive(Deserialize)] - struct ConnectionsResponse { - connections: HashMap, - } - - let resp: ConnectionsResponse = self.api_get("system/connections").await?; - Ok(resp.connections) - } - - /// Pause syncing - pub async fn pause(&self) -> CfkResult<()> { - let config = self.config.read().await; - let url = format!("{}/rest/system/pause", config.api_url); - - self.http - .post(&url) - .header("X-API-Key", &config.api_key) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - Ok(()) - } - - /// Resume syncing - pub async fn resume(&self) -> CfkResult<()> { - let config = self.config.read().await; - let url = format!("{}/rest/system/resume", config.api_url); - - self.http - .post(&url) - .header("X-API-Key", &config.api_key) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - Ok(()) - } -} - -/// Connection information -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ConnectionInfo { - pub connected: bool, - pub paused: bool, - pub address: String, - pub client_version: String, - #[serde(rename = "type")] - pub connection_type: String, -} diff --git a/czech-file-knife/cfk-providers/src/transport.rs b/czech-file-knife/cfk-providers/src/transport.rs deleted file mode 100644 index b0fc1e582..000000000 --- a/czech-file-knife/cfk-providers/src/transport.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! Transport layer support -//! -//! Low-level transport protocols: -//! - TCP: Traditional reliable transport -//! - QUIC: Modern UDP-based transport (HTTP/3) -//! - UDP: Unreliable datagram -//! - Unix sockets: Local IPC -//! - Named pipes: Windows IPC - -#![allow(dead_code)] // Placeholder structs for future implementation - -use cfk_core::{CfkError, CfkResult}; -use std::net::SocketAddr; -use tokio::net::TcpStream; - -/// Transport type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Transport { - Tcp, - Quic, - Udp, - Unix, - Pipe, - // Multicast transports - Pgm, // Pragmatic General Multicast (RFC 3208) - Norm, // NACK-Oriented Reliable Multicast (RFC 5740) - Rmtp, // Reliable Multicast Transport Protocol -} - -/// Connection configuration -#[derive(Debug, Clone)] -pub struct ConnectionConfig { - pub transport: Transport, - pub addr: String, - pub port: u16, - pub timeout_ms: u64, - pub keepalive: bool, - pub nodelay: bool, // TCP_NODELAY - pub buffer_size: usize, -} - -impl Default for ConnectionConfig { - fn default() -> Self { - Self { - transport: Transport::Tcp, - addr: "127.0.0.1".into(), - port: 0, - timeout_ms: 30000, - keepalive: true, - nodelay: true, - buffer_size: 65536, - } - } -} - -/// TCP connection wrapper -pub struct TcpConnection { - stream: TcpStream, - config: ConnectionConfig, -} - -impl TcpConnection { - pub async fn connect(config: ConnectionConfig) -> CfkResult { - let addr = format!("{}:{}", config.addr, config.port); - let stream = TcpStream::connect(&addr).await - .map_err(|e| CfkError::Network(e.to_string()))?; - - stream.set_nodelay(config.nodelay) - .map_err(|e| CfkError::Network(e.to_string()))?; - - Ok(Self { stream, config }) - } - - pub fn inner(&self) -> &TcpStream { - &self.stream - } - - pub fn into_inner(self) -> TcpStream { - self.stream - } -} - -/// QUIC configuration -#[derive(Debug, Clone)] -pub struct QuicConfig { - pub alpn_protocols: Vec, - pub max_idle_timeout_ms: u64, - pub keep_alive_interval_ms: Option, - pub max_concurrent_streams: u32, - pub initial_window_size: u32, -} - -impl Default for QuicConfig { - fn default() -> Self { - Self { - alpn_protocols: vec!["h3".into()], - max_idle_timeout_ms: 30000, - keep_alive_interval_ms: Some(15000), - max_concurrent_streams: 100, - initial_window_size: 1048576, // 1MB - } - } -} - -/// QUIC connection (stub - would use quinn crate) -pub struct QuicConnection { - config: QuicConfig, - // In real impl: quinn::Connection -} - -impl QuicConnection { - pub async fn connect(_addr: SocketAddr, _server_name: &str, config: QuicConfig) -> CfkResult { - // TODO: Implement with quinn crate - // let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?; - // let connection = endpoint.connect(addr, server_name)?.await?; - Ok(Self { config }) - } - - /// Open a new bidirectional stream - pub async fn open_stream(&self) -> CfkResult { - Err(CfkError::Unsupported("QUIC not yet implemented".into())) - } -} - -/// QUIC bidirectional stream -pub struct QuicStream { - // In real impl: quinn::SendStream + quinn::RecvStream -} - -/// Multi-transport connector -pub struct MultiTransport { - preferred: Transport, - fallback: Option, -} - -impl MultiTransport { - pub fn new(preferred: Transport) -> Self { - Self { preferred, fallback: None } - } - - pub fn with_fallback(mut self, fallback: Transport) -> Self { - self.fallback = Some(fallback); - self - } - - /// Connect using preferred transport, fall back if needed - pub async fn connect(&self, addr: &str, port: u16) -> CfkResult> { - let config = ConnectionConfig { - transport: self.preferred, - addr: addr.into(), - port, - ..Default::default() - }; - - match self.preferred { - Transport::Tcp => { - let conn = TcpConnection::connect(config).await?; - Ok(Box::new(conn)) - } - Transport::Quic => { - // Try QUIC, fall back to TCP if configured - if let Some(Transport::Tcp) = self.fallback { - let tcp_config = ConnectionConfig { - transport: Transport::Tcp, - addr: addr.into(), - port, - ..Default::default() - }; - let conn = TcpConnection::connect(tcp_config).await?; - Ok(Box::new(conn)) - } else { - Err(CfkError::Unsupported("QUIC not yet implemented".into())) - } - } - _ => Err(CfkError::Unsupported(format!("{:?} not implemented", self.preferred))), - } - } -} - -/// Abstract transport stream trait -pub trait TransportStream: Send + Sync { - fn transport_type(&self) -> Transport; -} - -impl TransportStream for TcpConnection { - fn transport_type(&self) -> Transport { - Transport::Tcp - } -} - -/// Reliable multicast support -pub mod multicast { - use super::*; - use std::net::Ipv4Addr; - - /// Multicast group configuration - #[derive(Debug, Clone)] - pub struct MulticastGroup { - pub group_addr: Ipv4Addr, - pub port: u16, - pub interface: Option, - pub ttl: u8, - pub loopback: bool, - } - - impl Default for MulticastGroup { - fn default() -> Self { - Self { - group_addr: Ipv4Addr::new(239, 255, 0, 1), // Local scope - port: 5000, - interface: None, - ttl: 1, - loopback: false, - } - } - } - - /// PGM (Pragmatic General Multicast) configuration - #[derive(Debug, Clone)] - pub struct PgmConfig { - pub group: MulticastGroup, - pub rate_limit_kbps: u32, - pub window_size: u32, - pub nak_rdata_ivl_ms: u32, // NAK repeat interval - } - - impl Default for PgmConfig { - fn default() -> Self { - Self { - group: MulticastGroup::default(), - rate_limit_kbps: 10000, // 10 Mbps - window_size: 1024, - nak_rdata_ivl_ms: 200, - } - } - } - - /// NORM (NACK-Oriented Reliable Multicast) configuration - #[derive(Debug, Clone)] - pub struct NormConfig { - pub group: MulticastGroup, - pub rate_kbps: u32, - pub buffer_size: usize, - pub segment_size: u16, - pub fec_enabled: bool, // Forward Error Correction - } - - impl Default for NormConfig { - fn default() -> Self { - Self { - group: MulticastGroup::default(), - rate_kbps: 10000, - buffer_size: 1048576, // 1MB - segment_size: 1400, - fec_enabled: true, - } - } - } - - /// Reliable multicast sender - pub struct MulticastSender { - transport: Transport, - // In real impl: PGM/NORM socket - } - - impl MulticastSender { - pub async fn new_pgm(_config: PgmConfig) -> CfkResult { - // TODO: Implement PGM sender - Err(CfkError::Unsupported("PGM multicast not yet implemented".into())) - } - - pub async fn new_norm(_config: NormConfig) -> CfkResult { - // TODO: Implement NORM sender - Err(CfkError::Unsupported("NORM multicast not yet implemented".into())) - } - - /// Send data to all group members - pub async fn send(&self, _data: &[u8]) -> CfkResult<()> { - Err(CfkError::Unsupported("Multicast send not implemented".into())) - } - - /// Send file to all group members with progress - pub async fn send_file(&self, _path: &std::path::Path) -> CfkResult<()> { - Err(CfkError::Unsupported("Multicast file send not implemented".into())) - } - } - - /// Reliable multicast receiver - pub struct MulticastReceiver { - transport: Transport, - } - - impl MulticastReceiver { - pub async fn join_pgm(_config: PgmConfig) -> CfkResult { - Err(CfkError::Unsupported("PGM multicast not yet implemented".into())) - } - - pub async fn join_norm(_config: NormConfig) -> CfkResult { - Err(CfkError::Unsupported("NORM multicast not yet implemented".into())) - } - - /// Receive data from group - pub async fn recv(&self) -> CfkResult> { - Err(CfkError::Unsupported("Multicast recv not implemented".into())) - } - } -} diff --git a/czech-file-knife/cfk-providers/src/webdav.rs b/czech-file-knife/cfk-providers/src/webdav.rs deleted file mode 100644 index 48e9edc4b..000000000 --- a/czech-file-knife/cfk-providers/src/webdav.rs +++ /dev/null @@ -1,611 +0,0 @@ -//! WebDAV storage backend -//! -//! HTTP-based distributed authoring and versioning protocol. -//! Compatible with NextCloud, ownCloud, SharePoint, Apache mod_dav, etc. - -use async_trait::async_trait; -use bytes::Bytes; -use cfk_core::{ - CfkError, CfkResult, Entry, EntryKind, Metadata, StorageBackend, StorageCapabilities, - VirtualPath, -}; -use chrono::{DateTime, Utc}; -use reqwest::{header, Client, Method, StatusCode}; -use serde::Deserialize; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// WebDAV authentication method -#[derive(Debug, Clone)] -pub enum WebDavAuth { - /// No authentication - None, - /// Basic authentication - Basic { username: String, password: String }, - /// Bearer token (OAuth) - Bearer(String), - /// Digest authentication (handled by reqwest) - Digest { username: String, password: String }, -} - -/// WebDAV backend configuration -#[derive(Debug, Clone)] -pub struct WebDavConfig { - /// Base URL (e.g., "https://cloud.example.com/remote.php/dav/files/username") - pub base_url: String, - /// Authentication method - pub auth: WebDavAuth, - /// Custom headers - pub headers: Vec<(String, String)>, -} - -/// WebDAV storage backend -pub struct WebDavBackend { - id: String, - config: Arc>, - http: Client, - capabilities: StorageCapabilities, -} - -impl WebDavBackend { - pub fn new(id: impl Into, config: WebDavConfig) -> Self { - Self { - id: id.into(), - config: Arc::new(RwLock::new(config)), - http: Client::new(), - capabilities: StorageCapabilities { - read: true, - write: true, - delete: true, - rename: true, - copy: true, - list: true, - search: false, - versioning: false, // Some servers support it - sharing: false, - streaming: true, - resume: true, - watch: false, - metadata: true, - thumbnails: false, - max_file_size: None, - }, - } - } - - /// Build authenticated request - async fn request(&self, method: Method, path: &str) -> reqwest::RequestBuilder { - let config = self.config.read().await; - let url = format!("{}/{}", config.base_url.trim_end_matches('/'), path.trim_start_matches('/')); - - let mut request = self.http.request(method, &url); - - match &config.auth { - WebDavAuth::None => {} - WebDavAuth::Basic { username, password } => { - request = request.basic_auth(username, Some(password)); - } - WebDavAuth::Bearer(token) => { - request = request.bearer_auth(token); - } - WebDavAuth::Digest { username, password } => { - // reqwest handles digest auth automatically - request = request.basic_auth(username, Some(password)); - } - } - - for (key, value) in &config.headers { - request = request.header(key, value); - } - - request - } - - /// Convert VirtualPath to URL path - fn to_url_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - String::new() - } else { - path.segments - .iter() - .map(|s| urlencoding::encode(s).to_string()) - .collect::>() - .join("/") - } - } - - /// PROPFIND request for listing/metadata - async fn propfind(&self, path: &str, depth: &str) -> CfkResult> { - let body = r#" - - - - - - - - - -"#; - - let response = self - .request(Method::from_bytes(b"PROPFIND").unwrap(), path) - .await - .header("Depth", depth) - .header(header::CONTENT_TYPE, "application/xml") - .body(body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != StatusCode::MULTI_STATUS { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - let text = response - .text() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - parse_multistatus(&text) - } -} - -/// DAV response from PROPFIND -#[derive(Debug, Clone, Default)] -struct DavResponse { - href: String, - is_collection: bool, - content_length: Option, - last_modified: Option>, - creation_date: Option>, - etag: Option, - content_type: Option, -} - -/// Parse WebDAV multistatus XML response -fn parse_multistatus(xml: &str) -> CfkResult> { - // Simple XML parsing without full XML crate - let mut responses = Vec::new(); - let mut current: Option = None; - - for line in xml.lines() { - let line = line.trim(); - - if line.contains("") || line.contains("") { - current = Some(DavResponse::default()); - } else if line.contains("") || line.contains("") { - if let Some(resp) = current.take() { - responses.push(resp); - } - } else if let Some(ref mut resp) = current { - // Parse href - if let Some(href) = extract_tag_content(line, "href") { - resp.href = urlencoding::decode(&href).unwrap_or(href.into()).to_string(); - } - - // Parse resourcetype - if line.contains(" Option { - let patterns = [ - format!("", tag), - format!("", tag), - format!("<{}:", tag), - ]; - - for pattern in &patterns { - if let Some(start) = line.find(pattern) { - let content_start = start + pattern.len(); - let end_patterns = [ - format!("", tag), - format!("", tag), - format!(" Option> { - // Try RFC 2822 first (most common) - if let Ok(dt) = DateTime::parse_from_rfc2822(s) { - return Some(dt.with_timezone(&Utc)); - } - - // Try RFC 3339 - if let Ok(dt) = DateTime::parse_from_rfc3339(s) { - return Some(dt.with_timezone(&Utc)); - } - - // Try common HTTP date format - let formats = [ - "%a, %d %b %Y %H:%M:%S GMT", - "%A, %d-%b-%y %H:%M:%S GMT", - "%a %b %e %H:%M:%S %Y", - ]; - - for fmt in &formats { - if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, fmt) { - return Some(DateTime::from_naive_utc_and_offset(dt, Utc)); - } - } - - None -} - -impl DavResponse { - fn to_entry(&self, backend_id: &str, base_href: &str) -> Entry { - // Extract relative path from href - let relative_path = self - .href - .trim_start_matches(base_href) - .trim_start_matches('/') - .trim_end_matches('/'); - - let kind = if self.is_collection { - EntryKind::Directory - } else { - EntryKind::File - }; - - let mut metadata = Metadata::default(); - metadata.size = self.content_length; - metadata.modified = self.last_modified; - metadata.created = self.creation_date; - metadata.checksum = self.etag.clone(); - metadata.mime_type = self.content_type.clone(); - - Entry { - path: VirtualPath::new(backend_id, relative_path), - kind, - metadata, - } - } -} - -#[async_trait] -impl StorageBackend for WebDavBackend { - fn id(&self) -> &str { - &self.id - } - - fn display_name(&self) -> &str { - "WebDAV" - } - - fn capabilities(&self) -> &StorageCapabilities { - &self.capabilities - } - - async fn is_available(&self) -> bool { - self.propfind("", "0").await.is_ok() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let url_path = self.to_url_path(path); - let responses = self.propfind(&url_path, "0").await?; - - responses - .first() - .map(|r| r.to_entry(&self.id, "")) - .ok_or_else(|| CfkError::NotFound(path.to_string())) - } - - async fn list_directory(&self, path: &VirtualPath) -> CfkResult> { - let url_path = self.to_url_path(path); - let responses = self.propfind(&url_path, "1").await?; - - // First response is the directory itself, skip it - Ok(responses - .iter() - .skip(1) - .map(|r| r.to_entry(&self.id, "")) - .collect()) - } - - async fn read_file(&self, path: &VirtualPath) -> CfkResult { - let url_path = self.to_url_path(path); - - let response = self - .request(Method::GET, &url_path) - .await - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - if status == StatusCode::NOT_FOUND { - return Err(CfkError::NotFound(path.to_string())); - } - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - response - .bytes() - .await - .map_err(|e| CfkError::Network(e.to_string())) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes) -> CfkResult { - let url_path = self.to_url_path(path); - - let response = self - .request(Method::PUT, &url_path) - .await - .header(header::CONTENT_TYPE, "application/octet-stream") - .body(data.to_vec()) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(path).await - } - - async fn delete(&self, path: &VirtualPath) -> CfkResult<()> { - let url_path = self.to_url_path(path); - - let response = self - .request(Method::DELETE, &url_path) - .await - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != StatusCode::NO_CONTENT { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - Ok(()) - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let url_path = self.to_url_path(path); - - let response = self - .request(Method::from_bytes(b"MKCOL").unwrap(), &url_path) - .await - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() && response.status() != StatusCode::CREATED { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(path).await - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_url_path(from); - let to_path = self.to_url_path(to); - - let config = self.config.read().await; - let dest_url = format!( - "{}/{}", - config.base_url.trim_end_matches('/'), - to_path.trim_start_matches('/') - ); - - let response = self - .request(Method::from_bytes(b"COPY").unwrap(), &from_path) - .await - .header("Destination", &dest_url) - .header("Overwrite", "T") - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() - && response.status() != StatusCode::CREATED - && response.status() != StatusCode::NO_CONTENT - { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(to).await - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath) -> CfkResult { - let from_path = self.to_url_path(from); - let to_path = self.to_url_path(to); - - let config = self.config.read().await; - let dest_url = format!( - "{}/{}", - config.base_url.trim_end_matches('/'), - to_path.trim_start_matches('/') - ); - - let response = self - .request(Method::from_bytes(b"MOVE").unwrap(), &from_path) - .await - .header("Destination", &dest_url) - .header("Overwrite", "T") - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - if !response.status().is_success() - && response.status() != StatusCode::CREATED - && response.status() != StatusCode::NO_CONTENT - { - let status = response.status(); - let error_text = response.text().await.unwrap_or_default(); - return Err(CfkError::ProviderApi { - provider: "webdav".into(), - message: format!("{}: {}", status, error_text), - }); - } - - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult<(u64, u64)> { - // WebDAV quota requires RFC 4331 support - let body = r#" - - - - - -"#; - - let response = self - .request(Method::from_bytes(b"PROPFIND").unwrap(), "") - .await - .header("Depth", "0") - .header(header::CONTENT_TYPE, "application/xml") - .body(body) - .send() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let text = response - .text() - .await - .map_err(|e| CfkError::Network(e.to_string()))?; - - let available = extract_tag_content(&text, "quota-available-bytes") - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let used = extract_tag_content(&text, "quota-used-bytes") - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let total = available + used; - Ok((available, total)) - } -} - -/// NextCloud-specific extensions -impl WebDavBackend { - /// Create a NextCloud backend with standard configuration - pub fn nextcloud( - id: impl Into, - server_url: &str, - username: &str, - password: &str, - ) -> Self { - let base_url = format!( - "{}/remote.php/dav/files/{}", - server_url.trim_end_matches('/'), - username - ); - - Self::new( - id, - WebDavConfig { - base_url, - auth: WebDavAuth::Basic { - username: username.to_string(), - password: password.to_string(), - }, - headers: vec![], - }, - ) - } - - /// Create an ownCloud backend - pub fn owncloud( - id: impl Into, - server_url: &str, - username: &str, - password: &str, - ) -> Self { - let base_url = format!( - "{}/remote.php/webdav", - server_url.trim_end_matches('/') - ); - - Self::new( - id, - WebDavConfig { - base_url, - auth: WebDavAuth::Basic { - username: username.to_string(), - password: password.to_string(), - }, - headers: vec![], - }, - ) - } -} diff --git a/czech-file-knife/cfk-search/Cargo.toml b/czech-file-knife/cfk-search/Cargo.toml deleted file mode 100644 index 7047c43cb..000000000 --- a/czech-file-knife/cfk-search/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "cfk-search" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Full-text search for Czech File Knife" - -[features] -default = [] -tantivy = ["dep:tantivy"] - -[dependencies] -cfk-core = { path = "../cfk-core" } - -# Search -tantivy = { workspace = true, optional = true } -regex.workspace = true - -# Async -tokio.workspace = true -async-trait.workspace = true - -# Serialization -serde.workspace = true - -# Time -chrono.workspace = true - -# Error handling -thiserror.workspace = true diff --git a/czech-file-knife/cfk-search/src/lib.rs b/czech-file-knife/cfk-search/src/lib.rs deleted file mode 100644 index a6d3e17d9..000000000 --- a/czech-file-knife/cfk-search/src/lib.rs +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Full-text search for Czech File Knife -//! -//! This module provides full-text search capabilities using Tantivy. -//! Currently a stub - full implementation coming in a future release. - -use async_trait::async_trait; -use cfk_core::{CfkResult, Entry, VirtualPath}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -/// Search index errors -#[derive(Error, Debug)] -pub enum SearchError { - #[error("Index not found: {0}")] - IndexNotFound(String), - - #[error("Index error: {0}")] - IndexError(String), - - #[error("Query parse error: {0}")] - QueryError(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Search result with relevance score -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SearchResult { - /// The matching entry - pub entry: Entry, - /// Relevance score (0.0 - 1.0) - pub score: f32, - /// Matching snippets with highlights - pub snippets: Vec, -} - -/// Search query options -#[derive(Debug, Clone, Default)] -pub struct SearchQuery { - /// The search query string - pub query: String, - /// Limit search to specific backends - pub backends: Option>, - /// Limit search to specific path prefixes - pub paths: Option>, - /// Maximum number of results - pub limit: Option, - /// Offset for pagination - pub offset: Option, - /// File type filters (e.g., "pdf", "txt") - pub file_types: Option>, - /// Search in file contents (not just names) - pub search_contents: bool, -} - -/// Search index trait -#[async_trait] -pub trait SearchIndex: Send + Sync { - /// Index a file or directory - async fn index(&self, entry: &Entry, content: Option<&[u8]>) -> CfkResult<()>; - - /// Remove an entry from the index - async fn remove(&self, path: &VirtualPath) -> CfkResult<()>; - - /// Search the index - async fn search(&self, query: &SearchQuery) -> CfkResult>; - - /// Clear the entire index - async fn clear(&self) -> CfkResult<()>; - - /// Get index statistics - async fn stats(&self) -> CfkResult; -} - -/// Index statistics -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct IndexStats { - /// Number of indexed documents - pub document_count: u64, - /// Index size in bytes - pub size_bytes: u64, - /// Last update timestamp - pub last_updated: Option>, -} - -/// Tantivy-based search index (stub) -/// Enable the `tantivy` feature to use this. -#[cfg(feature = "tantivy")] -pub struct TantivyIndex { - _path: PathBuf, -} - -#[cfg(feature = "tantivy")] -impl TantivyIndex { - /// Create a new Tantivy index at the given path - pub fn new(_path: impl Into) -> CfkResult { - Err(CfkError::Unsupported( - "Tantivy search index not yet implemented".into(), - )) - } - - /// Open an existing index - pub fn open(_path: impl Into) -> CfkResult { - Err(CfkError::Unsupported( - "Tantivy search index not yet implemented".into(), - )) - } -} - -#[cfg(feature = "tantivy")] -#[async_trait] -impl SearchIndex for TantivyIndex { - async fn index(&self, _entry: &Entry, _content: Option<&[u8]>) -> CfkResult<()> { - Err(CfkError::Unsupported("Search indexing not yet implemented".into())) - } - - async fn remove(&self, _path: &VirtualPath) -> CfkResult<()> { - Err(CfkError::Unsupported("Search indexing not yet implemented".into())) - } - - async fn search(&self, _query: &SearchQuery) -> CfkResult> { - Err(CfkError::Unsupported("Search not yet implemented".into())) - } - - async fn clear(&self) -> CfkResult<()> { - Err(CfkError::Unsupported("Search indexing not yet implemented".into())) - } - - async fn stats(&self) -> CfkResult { - Err(CfkError::Unsupported("Search indexing not yet implemented".into())) - } -} - -/// Simple filename-based search (works without full-text index) -pub async fn search_by_name( - pattern: &str, - entries: impl IntoIterator, -) -> Vec { - let pattern_lower = pattern.to_lowercase(); - entries - .into_iter() - .filter(|e| { - e.name() - .map(|n| n.to_lowercase().contains(&pattern_lower)) - .unwrap_or(false) - }) - .collect() -} - -/// Glob-style pattern matching -pub fn matches_glob(pattern: &str, name: &str) -> bool { - let pattern = pattern.to_lowercase(); - let name = name.to_lowercase(); - - if pattern == "*" { - return true; - } - - if let Some(suffix) = pattern.strip_prefix("*.") { - return name.ends_with(&format!(".{}", suffix)); - } - - if let Some(prefix) = pattern.strip_suffix(".*") { - return name.starts_with(prefix); - } - - name.contains(&pattern) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_matches_glob() { - assert!(matches_glob("*", "anything.txt")); - assert!(matches_glob("*.txt", "file.txt")); - assert!(matches_glob("*.TXT", "file.txt")); - assert!(!matches_glob("*.txt", "file.pdf")); - assert!(matches_glob("file.*", "file.txt")); - assert!(matches_glob("test", "my_test_file.txt")); - } -} diff --git a/czech-file-knife/cfk-tui/cfk_tui.gpr b/czech-file-knife/cfk-tui/cfk_tui.gpr deleted file mode 100644 index b1fb1ef3f..000000000 --- a/czech-file-knife/cfk-tui/cfk_tui.gpr +++ /dev/null @@ -1,38 +0,0 @@ --- Czech File Knife TUI --- Ada/SPARK Terminal User Interface - -project Cfk_Tui is - - for Source_Dirs use ("src", "spark"); - for Object_Dir use "obj"; - for Exec_Dir use "bin"; - for Main use ("cfk_tui_main.adb"); - - type Build_Mode is ("debug", "release", "spark"); - Mode : Build_Mode := external ("MODE", "debug"); - - package Compiler is - case Mode is - when "debug" => - for Default_Switches ("Ada") use - ("-g", "-gnata", "-gnatwa", "-gnatVa", "-gnatQ", - "-gnat2022", "-gnatyM120"); - when "release" => - for Default_Switches ("Ada") use - ("-O2", "-gnatn", "-gnatp", "-gnat2022"); - when "spark" => - for Default_Switches ("Ada") use - ("-g", "-gnata", "-gnat2022"); - end case; - end Compiler; - - package Prove is - for Proof_Switches ("Ada") use - ("--level=2", "--timeout=60", "--memlimit=2000"); - end Prove; - - package Binder is - for Default_Switches ("Ada") use ("-E"); - end Binder; - -end Cfk_Tui; diff --git a/czech-file-knife/cfk-tui/src/cfk-tui-application.ads b/czech-file-knife/cfk-tui/src/cfk-tui-application.ads deleted file mode 100644 index 7b7b2ce53..000000000 --- a/czech-file-knife/cfk-tui/src/cfk-tui-application.ads +++ /dev/null @@ -1,39 +0,0 @@ --- CFK TUI Application Package --- Main application state and lifecycle - -with CFK.TUI.Config; -with CFK.TUI.Screen; -with CFK.TUI.Input; - -package CFK.TUI.Application is - - type Application_State is (Initializing, Running, Paused, Exiting); - - type Application_Type is record - State : Application_State := Initializing; - Config : Config.Config_Type; - Screen : Screen.Screen_Type; - Input : Input.Input_Handler; - Exit_Code : Integer := 0; - end record; - - -- Lifecycle procedures - procedure Initialize - (App : in out Application_Type; - Config : Config.Config_Type); - - procedure Run (App : in out Application_Type); - - procedure Pause (App : in out Application_Type); - - procedure Resume (App : in out Application_Type); - - procedure Request_Exit (App : in out Application_Type; Code : Integer := 0); - - procedure Finalize (App : in out Application_Type); - - -- State queries - function Is_Running (App : Application_Type) return Boolean; - function Get_Exit_Code (App : Application_Type) return Integer; - -end CFK.TUI.Application; diff --git a/czech-file-knife/cfk-tui/src/cfk_tui_main.adb b/czech-file-knife/cfk-tui/src/cfk_tui_main.adb deleted file mode 100644 index b3dc5fe4f..000000000 --- a/czech-file-knife/cfk-tui/src/cfk_tui_main.adb +++ /dev/null @@ -1,36 +0,0 @@ --- Czech File Knife TUI Main --- Ada terminal user interface for CFK - -with Ada.Text_IO; -with Ada.Command_Line; -with CFK.TUI.Application; -with CFK.TUI.Config; - -procedure CFK_TUI_Main is - use Ada.Text_IO; - use Ada.Command_Line; - - App : CFK.TUI.Application.Application_Type; - Config : CFK.TUI.Config.Config_Type; -begin - -- Parse command line arguments - if Argument_Count > 0 then - Config := CFK.TUI.Config.Parse_Args; - else - Config := CFK.TUI.Config.Default_Config; - end if; - - -- Initialize application - CFK.TUI.Application.Initialize (App, Config); - - -- Run main loop - CFK.TUI.Application.Run (App); - - -- Cleanup - CFK.TUI.Application.Finalize (App); - -exception - when E : others => - Put_Line (Standard_Error, "Fatal error in CFK TUI"); - Set_Exit_Status (Failure); -end CFK_TUI_Main; diff --git a/czech-file-knife/cfk-vfs/Cargo.toml b/czech-file-knife/cfk-vfs/Cargo.toml deleted file mode 100644 index 1f24a9de2..000000000 --- a/czech-file-knife/cfk-vfs/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "cfk-vfs" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "FUSE virtual filesystem for Czech File Knife" - -[features] -default = [] -fuse = ["dep:fuser", "dep:parking_lot", "dep:dashmap"] - -[dependencies] -cfk-core = { path = "../cfk-core" } -cfk-providers = { path = "../cfk-providers" } - -# FUSE (optional - requires libfuse on system) -fuser = { workspace = true, optional = true } -parking_lot = { workspace = true, optional = true } -dashmap = { workspace = true, optional = true } - -# Async -tokio.workspace = true -async-trait.workspace = true - -# Error handling -thiserror.workspace = true - -# Logging -tracing.workspace = true diff --git a/czech-file-knife/cfk-vfs/src/lib.rs b/czech-file-knife/cfk-vfs/src/lib.rs deleted file mode 100644 index 467746f10..000000000 --- a/czech-file-knife/cfk-vfs/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! FUSE virtual filesystem for Czech File Knife -//! -//! This module provides FUSE mounting capabilities to access -//! any CFK backend as a local filesystem. -//! Currently a stub - full implementation coming in a future release. - -use cfk_core::{CfkError, CfkResult}; -use std::path::PathBuf; -use thiserror::Error; - -/// VFS errors -#[derive(Error, Debug)] -pub enum VfsError { - #[error("Mount point does not exist: {0}")] - MountPointNotFound(String), - - #[error("Mount point is not a directory: {0}")] - MountPointNotDirectory(String), - - #[error("Already mounted at: {0}")] - AlreadyMounted(String), - - #[error("Not mounted")] - NotMounted, - - #[error("FUSE error: {0}")] - Fuse(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Mount options -#[derive(Debug, Clone, Default)] -pub struct MountOptions { - /// Allow other users to access the mount - pub allow_other: bool, - /// Allow root to access the mount - pub allow_root: bool, - /// Read-only mount - pub read_only: bool, - /// Enable caching - pub cache: bool, - /// Cache timeout in seconds - pub cache_timeout_secs: Option, - /// Debug mode - pub debug: bool, -} - -/// VFS mount handle -pub struct VfsMount { - mount_point: PathBuf, - _options: MountOptions, -} - -impl VfsMount { - /// Mount a CFK backend at the given path - /// - /// # Arguments - /// * `backend_id` - The backend to mount (e.g., "local", "dropbox") - /// * `mount_point` - The local path to mount at - /// * `options` - Mount options - pub fn mount( - _backend_id: &str, - _mount_point: impl Into, - _options: MountOptions, - ) -> CfkResult { - Err(CfkError::Unsupported( - "FUSE VFS mounting not yet implemented".into(), - )) - } - - /// Get the mount point path - pub fn mount_point(&self) -> &PathBuf { - &self.mount_point - } - - /// Check if the mount is still active - pub fn is_mounted(&self) -> bool { - false - } - - /// Unmount the filesystem - pub fn unmount(self) -> CfkResult<()> { - Err(CfkError::Unsupported( - "FUSE VFS mounting not yet implemented".into(), - )) - } -} - -impl Drop for VfsMount { - fn drop(&mut self) { - // Attempt to unmount on drop - // In real implementation, this would call fuser::unmount - } -} - -/// List active mounts -pub fn list_mounts() -> Vec { - Vec::new() -} - -/// Check if FUSE is available on this system -pub fn is_fuse_available() -> bool { - #[cfg(target_os = "linux")] - { - std::path::Path::new("/dev/fuse").exists() - } - - #[cfg(target_os = "macos")] - { - // Check for macFUSE - std::path::Path::new("/Library/Filesystems/macfuse.fs").exists() - } - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_fuse_available() { - // Just make sure it doesn't panic - let _ = is_fuse_available(); - } - - #[test] - fn test_mount_not_implemented() { - let result = VfsMount::mount("local", "/tmp/test", MountOptions::default()); - assert!(result.is_err()); - } -} diff --git a/czech-file-knife/contractiles/README.adoc b/czech-file-knife/contractiles/README.adoc deleted file mode 100644 index d19a38774..000000000 --- a/czech-file-knife/contractiles/README.adoc +++ /dev/null @@ -1,19 +0,0 @@ -= Contractiles Template Set -:toc: -:sectnums: - -This directory contains the generalized contractiles templates. Copy the `contractiles/` directory into a new repo to establish a consistent operational, validation, trust, recovery, and intent framework. - -== Fill-In Instructions - -1. Update the Mustfile to reflect your real invariants (paths, schema versions, ports). -2. Replace Trustfile.hs placeholders with your actual key paths and verification commands. -3. Adjust Dustfile handlers to match your rollback and recovery tooling. -4. Update Intentfile to mirror the roadmap you want the system to evolve toward. - -== Contents - -* `must/Mustfile` - required invariants and validations. -* `trust/Trustfile.hs` - cryptographic verification steps. -* `dust/Dustfile` - rollback and recovery semantics. -* `lust/Intentfile` - future intent and roadmap direction. diff --git a/czech-file-knife/contractiles/dust/Dustfile b/czech-file-knife/contractiles/dust/Dustfile deleted file mode 100644 index 314903cca..000000000 --- a/czech-file-knife/contractiles/dust/Dustfile +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Dustfile template - recovery and rollback semantics - -version: 1 - -recovery: - logs: - - name: decision-log - path: logs/decisions.json - reversible: true - handler: "log-replay --reverse logs/decisions.json" - - policy: - - name: policy-rollback - path: policy/policy.ncl - rollback: "git checkout HEAD~1 -- policy/policy.ncl" - notes: "Rollback policy to the previous known-good revision." - - gateway: - - name: bad-deployment - event: "deploy.failure" - undo: "kubectl rollout undo deployment/gateway" - notes: "Undo a failed deployment while preserving audit logs." - - dust-events: - - name: decision-log-to-dust - source: logs/decisions.json - transform: "dustify --input logs/decisions.json --output logs/dust-events.json" - notes: "Map gateway decision logs into reversible dust events." diff --git a/czech-file-knife/contractiles/must/Mustfile b/czech-file-knife/contractiles/must/Mustfile deleted file mode 100644 index dc7b3be51..000000000 --- a/czech-file-knife/contractiles/must/Mustfile +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Mustfile - declarative state contract (template) -# See: https://github.com/hyperpolymath/mustfile - -version: 1 - -metadata: - name: project-state-contract - spec: v0.0.1 - description: "Invariant checks for config, policy, gateway, logs, and schema." - -parameters: - gateway_port: "8080" - schema_version: "v0.0.1" - -checks: - - name: config-valid - description: "config/service.yaml must be valid." - run: "yq -e '.' config/service.yaml >/dev/null" - - - name: policy-compiles - description: "policy/policy.ncl must compile." - run: "nickel check policy/policy.ncl" - - - name: gateway-exposes-port - description: "Service must expose the configured port." - run: "bash -uc 'ss -lnt | rg \":${GATEWAY_PORT:-8080}\"'" - - - name: logs-are-json - description: "Logs must be JSON." - run: "bash -uc 'rg --files -g \"*.json\" logs | xargs -r jq -e .'" - - - name: schema-version-matches - description: "Schema must match version spec." - run: "bash -uc 'rg -n \"${SCHEMA_VERSION:-v0.0.1}\" schema'" diff --git a/czech-file-knife/deny.toml b/czech-file-knife/deny.toml deleted file mode 100644 index 90f29229c..000000000 --- a/czech-file-knife/deny.toml +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# cargo-deny configuration for czech-file-knife -# https://embarkstudios.github.io/cargo-deny/ - -[graph] -targets = [] -all-features = true - -[advisories] -db-path = "~/.cargo/advisory-db" -db-urls = ["https://github.com/rustsec/advisory-db"] -vulnerability = "deny" -unmaintained = "warn" -yanked = "warn" -notice = "warn" -ignore = [] - -[licenses] -version = 2 -allow = [ - "MIT", - "Apache-2.0", - "Apache-2.0 WITH LLVM-exception", - "BSD-2-Clause", - "BSD-3-Clause", - "ISC", - "Zlib", - "0BSD", - "Unicode-DFS-2016", - "AGPL-3.0-or-later", - "GPL-3.0-or-later", - "LGPL-3.0-or-later", - "MPL-2.0", - "CC0-1.0", - "Unlicense", -] -confidence-threshold = 0.8 -exceptions = [] - -[[licenses.clarify]] -name = "ring" -expression = "MIT AND ISC AND OpenSSL" -license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] - -[bans] -multiple-versions = "warn" -wildcards = "allow" -highlight = "all" -workspace-default-features = "allow" -external-default-features = "allow" -allow = [] -deny = [] -skip = [] -skip-tree = [] - -[sources] -unknown-registry = "deny" -unknown-git = "warn" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] diff --git a/czech-file-knife/docs/CITATIONS.adoc b/czech-file-knife/docs/CITATIONS.adoc deleted file mode 100644 index 5b68cf7b1..000000000 --- a/czech-file-knife/docs/CITATIONS.adoc +++ /dev/null @@ -1,36 +0,0 @@ -= czech-file-knife - Citation Guide -:toc: - -== BibTeX - -[source,bibtex] ----- -@software{czech-file-knife_2025, - author = {Polymath, Hyper}, - title = {czech-file-knife}, - year = {2025}, - url = {https://github.com/hyperpolymath/czech-file-knife}, - license = {AGPL-3.0-or-later} -} ----- - -== Harvard Style - -Polymath, H. (2025) _czech-file-knife_ [Computer software]. Available at: https://github.com/hyperpolymath/czech-file-knife - -== OSCOLA - -Hyper Polymath, 'czech-file-knife' (2025) - -== MLA - -Polymath, Hyper. "czech-file-knife." 2025, github.com/hyperpolymath/czech-file-knife. - -== APA 7 - -Polymath, H. (2025). _czech-file-knife_ [Computer software]. GitHub. https://github.com/hyperpolymath/czech-file-knife - -== See Also - -* link:../CITATION.cff[CITATION.cff] -* link:../codemeta.json[codemeta.json] diff --git a/czech-file-knife/docs/DISTRIBUTED_FILESYSTEMS.adoc b/czech-file-knife/docs/DISTRIBUTED_FILESYSTEMS.adoc deleted file mode 100644 index 9954ff73f..000000000 --- a/czech-file-knife/docs/DISTRIBUTED_FILESYSTEMS.adoc +++ /dev/null @@ -1,380 +0,0 @@ -== Distributed Filesystems Support - -Czech File Knife (CFK) provides unified access to various distributed -and network filesystems through its provider abstraction layer. - -=== Supported Filesystems - -==== Network File Systems - -===== NFS (Network File System) - -* *Module*: `+cfk-providers/src/nfs.rs+` -* *Feature*: `+nfs+` -* *Versions*: NFSv3, NFSv4, NFSv4.1 -* *Status*: Stub (uses system mount) - -[source,rust] ----- -use cfk_providers::nfs::{NfsBackend, NfsConfig, NfsVersion}; - -let config = NfsConfig { - server: "nas.local".into(), - export_path: "/exports/data".into(), - version: NfsVersion::V4, - ..Default::default() -}; - -let backend = NfsBackend::new("my-nfs", config); ----- - -===== SMB/CIFS (Server Message Block) - -* *Module*: `+cfk-providers/src/smb.rs+` -* *Feature*: `+smb+` -* *Versions*: SMB2, SMB3, SMB3.1.1 -* *Status*: Stub (uses system mount or libsmbclient) - -[source,rust] ----- -use cfk_providers::smb::{SmbBackend, SmbConfig, SmbVersion}; - -let config = SmbConfig { - server: "fileserver".into(), - share: "Documents".into(), - username: Some("user".into()), - password: Some("pass".into()), - version: SmbVersion::Smb3, - ..Default::default() -}; - -let backend = SmbBackend::new("my-smb", config); ----- - -===== SFTP (SSH File Transfer Protocol) - -* *Module*: `+cfk-providers/src/sftp.rs+` -* *Feature*: `+sftp+` -* *Status*: Stub (requires ssh2 or russh crate) - -[source,rust] ----- -use cfk_providers::sftp::{SftpBackend, SftpConfig, SftpAuth}; - -let config = SftpConfig { - host: "server.example.com".into(), - port: 22, - auth: SftpAuth::Agent { username: "user".into() }, - ..Default::default() -}; - -let backend = SftpBackend::new("my-sftp", config); ----- - -==== Plan 9 Protocol - -===== 9P (Plan 9 File Protocol) - -* *Module*: `+cfk-providers/src/ninep.rs+` -* *Feature*: `+ninep+` -* *Versions*: 9P2000, 9P2000.L, 9P2000.u -* *Use Cases*: WSL2 file sharing, QEMU virtio-9p, Plan 9 systems - -[source,rust] ----- -use cfk_providers::ninep::{NinePBackend, NinePConfig, NinePVersion}; - -// WSL2 connection -let backend = NinePBackend::wsl2("my-9p", "/mnt/c"); - -// QEMU virtio-9p -let config = NinePConfig { - transport: "virtio".into(), - aname: "shared".into(), - version: NinePVersion::L, - ..Default::default() -}; -let backend = NinePBackend::new("qemu-9p", config); ----- - -==== Distributed Storage Systems - -===== Ceph - -* *Module*: `+cfk-providers/src/ceph.rs+` -* *Feature*: `+ceph+` -* *Interfaces*: RADOS, CephFS, RGW (S3-compatible) - -[source,rust] ----- -use cfk_providers::ceph::{CephBackend, CephConfig, CephMode}; - -// CephFS (POSIX-like) -let config = CephConfig { - monitors: vec!["mon1:6789".into(), "mon2:6789".into()], - mode: CephMode::Fs { - fs_name: "cephfs".into(), - mount_point: "/mnt/ceph".into(), - }, - user: "admin".into(), - keyring_path: Some("/etc/ceph/ceph.client.admin.keyring".into()), - ..Default::default() -}; - -let backend = CephBackend::new("my-ceph", config); - -// RGW (S3-compatible) -let rgw_backend = CephBackend::rgw( - "my-rgw", - "https://rgw.example.com", - "access_key", - "secret_key", - "my-bucket" -); ----- - -===== IPFS (InterPlanetary File System) - -* *Module*: `+cfk-providers/src/ipfs.rs+` -* *Feature*: `+ipfs+` -* *Features*: Content-addressing, MFS, Pinning, IPNS - -[source,rust] ----- -use cfk_providers::ipfs::{IpfsBackend, IpfsConfig}; - -let config = IpfsConfig { - api_url: "http://localhost:5001".into(), - gateway_url: Some("http://localhost:8080".into()), - use_mfs: true, // Mutable File System - mfs_root: "/cfk".into(), - ..Default::default() -}; - -let backend = IpfsBackend::new("my-ipfs", config); ----- - -===== AFS (Andrew File System) - -* *Module*: `+cfk-providers/src/afs.rs+` -* *Feature*: `+afs+` -* *Features*: Kerberos auth, ACLs, distributed cells - -[source,rust] ----- -use cfk_providers::afs::{AfsBackend, AfsConfig}; - -let config = AfsConfig { - cell: "example.edu".into(), - realm: Some("EXAMPLE.EDU".into()), - cache_dir: Some("/var/cache/openafs".into()), - afs_mount: "/afs".into(), - ..Default::default() -}; - -let backend = AfsBackend::new("my-afs", config); ----- - -==== Cloud Storage - -===== S3-Compatible - -* *Module*: `+cfk-providers/src/s3.rs+` -* *Feature*: `+s3+` -* *Providers*: AWS, MinIO, Cloudflare R2, Backblaze B2, DigitalOcean -Spaces, Wasabi - -[source,rust] ----- -use cfk_providers::s3::{S3Backend, S3Config}; - -// AWS S3 -let backend = S3Backend::aws( - "my-s3", - "my-bucket", - "us-west-2", - "AKIAIOSFODNN7EXAMPLE", - "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" -); - -// MinIO -let backend = S3Backend::minio( - "my-minio", - "http://localhost:9000", - "my-bucket", - "minioadmin", - "minioadmin" -); - -// Cloudflare R2 -let backend = S3Backend::cloudflare_r2( - "my-r2", - "account-id", - "my-bucket", - "access_key", - "secret_key" -); ----- - -===== WebDAV - -* *Module*: `+cfk-providers/src/webdav.rs+` -* *Feature*: `+webdav+` -* *Servers*: NextCloud, ownCloud, Apache mod_dav, nginx - -[source,rust] ----- -use cfk_providers::webdav::{WebDavBackend, WebDavConfig, WebDavAuth}; - -// Generic WebDAV -let config = WebDavConfig { - base_url: "https://dav.example.com/files/".into(), - auth: Some(WebDavAuth::Basic { - username: "user".into(), - password: "pass".into(), - }), - ..Default::default() -}; - -let backend = WebDavBackend::new("my-webdav", config); - -// NextCloud -let backend = WebDavBackend::nextcloud( - "my-nextcloud", - "https://cloud.example.com", - "username", - "app-password" -); ----- - -=== Feature Comparison - -[cols=",,,,,,,,,",options="header",] -|=== -|Feature |NFS |SMB |SFTP |9P |Ceph |IPFS |AFS |S3 |WebDAV -|Read |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ -|Write |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ -|Delete |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ -|Rename |✓ |✓ |✓ |✓ |✓ |✗ |✓ |✗¹ |✓ -|Copy |✓ |✓ |✗ |✓ |✓ |✗ |✓ |✓² |✓ -|List |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ -|Streaming |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ |✓ -|Resume |✗ |✗ |✓ |✗ |✗ |✓ |✗ |✓ |✓ -|Versioning |✗ |✓³ |✗ |✗ |✗ |✓⁴ |✗ |✓ |✓ -|Watch |✗ |✓ |✗ |✗ |✗ |✗ |✗ |✗ |✗ -|ACLs |✓ |✓ |✓ |✗ |✓ |✗ |✓ |✓ |✗ -|=== - -¹ S3 rename is copy+delete ² S3 copy is server-side for same bucket ³ -SMB Previous Versions ⁴ IPFS content is immutable; IPNS provides -mutability - -=== Performance Considerations - -==== Latency - -[cols=",,",options="header",] -|=== -|Protocol |Typical Latency |Best For -|NFS v4 |1-10ms (LAN) |Enterprise storage -|SMB 3 |1-10ms (LAN) |Windows environments -|SFTP |10-100ms |Secure remote access -|9P |<1ms (virtio) |VM/container sharing -|Ceph |1-5ms |Scalable storage -|IPFS |Variable |Content distribution -|S3 |50-200ms |Object storage -|WebDAV |50-500ms |Web-based access -|=== - -==== Caching Strategy - -CFK automatically uses the caching layer (`+cfk-cache+`) to optimize -performance: - -[arabic] -. *Metadata Cache*: TTL-based caching of file/directory metadata -. *Content Cache*: Content-addressed blob storage with LZ4 compression -. *Eviction Policies*: LRU, LFU, FIFO, or adaptive policies - -[source,rust] ----- -use cfk_cache::{CachePolicy, PolicyConfig, EvictionPolicy}; - -let policy = CachePolicy::new(PolicyConfig { - max_size: 10 * 1024 * 1024 * 1024, // 10 GB - max_entries: 100_000, - eviction_policy: EvictionPolicy::Adaptive, - ..Default::default() -}); ----- - -=== Security - -==== Authentication Methods - -[cols=",",options="header",] -|=== -|Protocol |Methods -|NFS |Kerberos (sec=krb5), AUTH_SYS -|SMB |NTLM, Kerberos, Guest -|SFTP |Password, Public Key, Agent -|9P |None (transport security), Custom -|Ceph |Cephx, None -|IPFS |None (public), Key-based -|AFS |Kerberos -|S3 |AWS Sig V4, IAM -|WebDAV |Basic, Digest, OAuth -|=== - -==== Encryption - -[cols=",,",options="header",] -|=== -|Protocol |In-Transit |At-Rest -|NFS v4 |Optional (krb5p) |No -|SMB 3 |Yes (AES-128-GCM) |No -|SFTP |Yes (SSH) |No -|9P |No (use TLS wrapper) |No -|Ceph |Optional |Optional -|IPFS |Optional |No -|AFS |Yes |No -|S3 |Yes (HTTPS) |Optional (SSE) -|WebDAV |Yes (HTTPS) |No -|=== - -=== Enabling Features - -Add the desired features to your `+Cargo.toml+`: - -[source,toml] ----- -[dependencies] -cfk-providers = { path = "../cfk-providers", features = [ - "nfs", - "smb", - "sftp", - "ninep", - "ceph", - "ipfs", - "afs", - "s3", - "webdav", -] } ----- - -=== Architecture - -.... -┌─────────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (cfk-cli, cfk-vfs, cfk-tui) │ -├─────────────────────────────────────────────────────────────────────┤ -│ StorageBackend Trait │ -│ (unified interface for all filesystem operations) │ -├───────┬───────┬───────┬───────┬───────┬───────┬───────┬────────────┤ -│ NFS │ SMB │ SFTP │ 9P │ Ceph │ IPFS │ AFS │ S3/WebDAV │ -├───────┴───────┴───────┴───────┴───────┴───────┴───────┴────────────┤ -│ Cache Layer (cfk-cache) │ -│ (metadata cache, blob store, eviction) │ -└─────────────────────────────────────────────────────────────────────┘ -.... diff --git a/czech-file-knife/docs/IOS_INTEGRATION.adoc b/czech-file-knife/docs/IOS_INTEGRATION.adoc deleted file mode 100644 index 3c8031aac..000000000 --- a/czech-file-knife/docs/IOS_INTEGRATION.adoc +++ /dev/null @@ -1,394 +0,0 @@ -== iOS Integration Guide - -Czech File Knife (CFK) provides native iOS integration through Apple’s -File Provider framework, allowing cloud storage providers to appear in -the iOS Files app. - -=== Architecture - -.... -┌─────────────────────────────────────────────────────────────────┐ -│ iOS Files App │ -├─────────────────────────────────────────────────────────────────┤ -│ File Provider Framework │ -│ (NSFileProviderExtension) │ -├─────────────────────────────────────────────────────────────────┤ -│ Swift Wrapper Layer │ -│ (CfkFileProviderItem, CfkFileProviderExtension) │ -├─────────────────────────────────────────────────────────────────┤ -│ C FFI Bridge │ -│ (CfkBridge.h, ffi.rs) │ -├─────────────────────────────────────────────────────────────────┤ -│ Rust Core Library │ -│ (cfk-ios, cfk-core, cfk-providers) │ -└─────────────────────────────────────────────────────────────────┘ -.... - -=== Components - -==== cfk-ios Crate - -The `+cfk-ios+` crate provides: - -* *error.rs*: iOS-specific error types mapping to -`+NSFileProviderError+` -* *domain.rs*: File Provider domain management -* *item.rs*: `+NSFileProviderItem+` representation -* *provider.rs*: Main provider manager coordinating backends -* *ffi.rs*: C FFI layer for Swift interop - -==== Swift Integration - -Swift files in `+cfk-ios/swift/+`: - -* *CfkBridge.h*: C header for bridging -* *CfkFileProviderItem.swift*: `+NSFileProviderItem+` implementation -* *CfkFileProviderExtension.swift*: -`+NSFileProviderReplicatedExtension+` implementation - -=== Building for iOS - -==== Prerequisites - -[arabic] -. Xcode 14+ with iOS 16+ SDK -. Rust with iOS targets: - -[source,bash] ----- -rustup target add aarch64-apple-ios -rustup target add aarch64-apple-ios-sim # For simulator ----- - -[arabic, start=3] -. `+cargo-lipo+` for universal binaries (optional): - -[source,bash] ----- -cargo install cargo-lipo ----- - -==== Build Static Library - -[source,bash] ----- -# For device -cargo build --release --target aarch64-apple-ios -p cfk-ios - -# For simulator -cargo build --release --target aarch64-apple-ios-sim -p cfk-ios - -# Universal binary (both architectures) -cargo lipo --release -p cfk-ios ----- - -The static library will be at: - Device: -`+target/aarch64-apple-ios/release/libcfk_ios.a+` - Simulator: -`+target/aarch64-apple-ios-sim/release/libcfk_ios.a+` - -=== Xcode Project Setup - -==== 1. Create File Provider Extension - -[arabic] -. In Xcode, File → New → Target -. Select "`File Provider Extension`" -. Name it (e.g., "`CfkFileProvider`") - -==== 2. Add Static Library - -[arabic] -. Drag `+libcfk_ios.a+` into your project -. In target settings → Build Phases → Link Binary: -* Add `+libcfk_ios.a+` -* Add `+libresolv.tbd+` (for networking) - -==== 3. Configure Bridging Header - -[arabic] -. Create `+YourExtension-Bridging-Header.h+` -. Add: - -[source,objc] ----- -#import "CfkBridge.h" ----- - -[arabic, start=3] -. In Build Settings → Swift Compiler → Objective-C Bridging Header: -* Set to `+$(SRCROOT)/YourExtension/YourExtension-Bridging-Header.h+` - -==== 4. Add Swift Files - -Copy the Swift files from `+cfk-ios/swift/+` into your extension: - -`+CfkFileProviderItem.swift+` - `+CfkFileProviderExtension.swift+` - -==== 5. Configure Info.plist - -[source,xml] ----- -NSExtension - - NSExtensionFileProviderDocumentGroup - group.com.yourcompany.cfk - NSExtensionPointIdentifier - com.apple.fileprovider-nonui - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).CfkFileProviderExtension - NSExtensionFileProviderSupportsEnumeration - - ----- - -==== 6. Configure Entitlements - -[source,xml] ----- -com.apple.developer.fileprovider.testing-mode - -com.apple.security.application-groups - - group.com.yourcompany.cfk - ----- - -=== Usage - -==== Register Domains - -In your main app, register file provider domains: - -[source,swift] ----- -import FileProvider - -class StorageManager { - func addDropboxAccount() async throws { - // Initialize CFK - let container = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: "group.com.yourcompany.cfk" - )! - - try initializeCfk( - storagePath: container.appendingPathComponent("storage"), - cachePath: container.appendingPathComponent("cache"), - tempPath: container.appendingPathComponent("temp") - ) - - // Add domain via FFI - let result = "dropbox-main".withCString { id in - "Dropbox".withCString { name in - "dropbox".withCString { backend in - cfk_domain_add(id, name, backend, nil) - } - } - } - - guard result == CFK_ERROR_SUCCESS else { - throw CfkError.from(code: result) - } - - // Register with system - let domain = NSFileProviderDomain( - identifier: NSFileProviderDomainIdentifier("dropbox-main"), - displayName: "Dropbox" - ) - - try await NSFileProviderManager.add(domain) - } -} ----- - -==== Custom Extension Class - -Subclass `+CfkFileProviderExtension+` for customization: - -[source,swift] ----- -@available(iOS 16.0, *) -class MyFileProviderExtension: CfkFileProviderExtension { - - override func item( - for identifier: NSFileProviderItemIdentifier, - request: NSFileProviderRequest, - completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void - ) -> Progress { - // Custom handling - return super.item(for: identifier, request: request, completionHandler: completionHandler) - } -} ----- - -=== Handling Authentication - -==== OAuth Flow - -For cloud providers requiring OAuth: - -[source,swift] ----- -import AuthenticationServices - -class AuthManager { - func authenticateDropbox() async throws -> String { - // Build OAuth URL - let authURL = URL(string: "https://www.dropbox.com/oauth2/authorize?...")! - - // Present auth session - let callbackURL = try await withCheckedThrowingContinuation { continuation in - let session = ASWebAuthenticationSession( - url: authURL, - callbackURLScheme: "cfk" - ) { url, error in - if let error = error { - continuation.resume(throwing: error) - } else if let url = url { - continuation.resume(returning: url) - } - } - session.presentationContextProvider = self - session.start() - } - - // Extract token from callback - let token = extractToken(from: callbackURL) - - // Store token and configure backend - let config = """ - {"access_token": "\(token)"} - """ - - let result = "dropbox-main".withCString { id in - "Dropbox".withCString { name in - "dropbox".withCString { backend in - config.withCString { cfg in - cfk_domain_add(id, name, backend, cfg) - } - } - } - } - - return token - } -} ----- - -=== File Coordination - -For proper file coordination with other apps: - -[source,swift] ----- -func coordinatedRead(at url: URL) async throws -> Data { - let coordinator = NSFileCoordinator() - var error: NSError? - var data: Data? - - coordinator.coordinate(readingItemAt: url, options: [], error: &error) { coordURL in - data = try? Data(contentsOf: coordURL) - } - - if let error = error { - throw error - } - - return data ?? Data() -} ----- - -=== Thumbnails - -Implement thumbnail provider for preview support: - -[source,swift] ----- -@available(iOS 16.0, *) -class CfkThumbnailProvider: NSFileProviderThumbnailRequest { - - func fetchThumbnails( - for itemIdentifiers: [NSFileProviderItemIdentifier], - requestedSize size: CGSize, - perThumbnailCompletionHandler: @escaping ( - NSFileProviderItemIdentifier, - Data?, - Error? - ) -> Void, - completionHandler: @escaping (Error?) -> Void - ) -> Progress { - // Fetch thumbnails from backend - // ... - } -} ----- - -=== Testing - -==== Simulator Testing - -Enable File Provider testing in simulator: - -[arabic] -. Build and run extension -. In Simulator → Features → Enable File Provider Testing - -==== Device Testing - -[arabic] -. Enable Developer Mode on device -. Install provisioning profile with File Provider entitlement -. Build and run - -==== Debug Logging - -Enable verbose logging: - -[source,swift] ----- -#if DEBUG -cfk_ios_init() // Enables tracing in debug builds -#endif ----- - -=== Troubleshooting - -==== Common Issues - -[arabic] -. *Extension not appearing in Files* -* Check entitlements -* Verify NSExtension configuration in Info.plist -* Ensure domain is registered -. *Authentication failures* -* Check OAuth callback URL scheme -* Verify token storage -. *Crashes on launch* -* Verify static library is linked -* Check bridging header path -* Ensure `+cfk_ios_init()+` is called first -. *Slow enumeration* -* Enable caching -* Implement pagination properly - -==== Memory Considerations - -File Provider extensions have limited memory. Best practices: - -* Use streaming for large files -* Implement proper pagination -* Release cached items when memory warnings occur - -[source,swift] ----- -override func didReceiveMemoryWarning() { - // Release non-essential cached data -} ----- - -=== Resources - -* https://developer.apple.com/documentation/fileprovider[Apple File -Provider Documentation] -* https://developer.apple.com/videos/play/wwdc2017/243/[WWDC 2017: File -Provider Enhancements] -* https://developer.apple.com/videos/play/wwdc2021/10182/[WWDC 2021: -Meet the File Provider Replicated Extension] diff --git a/czech-file-knife/examples/web-project-deno.json b/czech-file-knife/examples/web-project-deno.json deleted file mode 100644 index eaa39d95c..000000000 --- a/czech-file-knife/examples/web-project-deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "// NOTE": "Example deno.json for ReScript web projects", - "tasks": { - "build": "deno run -A npm:rescript", - "clean": "deno run -A npm:rescript clean", - "watch": "deno run -A npm:rescript -w", - "serve": "deno run -A jsr:@std/http/file-server .", - "test": "deno test --allow-all" - }, - "imports": { - "rescript": "npm:rescript@^12.1.0", - "@rescript/core": "npm:@rescript/core@^1.6.0", - "safe-dom/": "https://raw.githubusercontent.com/hyperpolymath/rescript-dom-mounter/main/src/", - "proven/": "../proven/bindings/rescript/src/" - }, - "compilerOptions": { - "allowJs": true, - "checkJs": false - } -} diff --git a/czech-file-knife/fuzz/Cargo.toml b/czech-file-knife/fuzz/Cargo.toml deleted file mode 100644 index b0902daa3..000000000 --- a/czech-file-knife/fuzz/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -[package] -name = "czech-file-knife-fuzz" -version = "0.0.0" -authors = ["hyperpolymath"] -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" -arbitrary = { version = "1", features = ["derive"] } - -[dependencies.cfk-core] -path = "../cfk-core" - -[[bin]] -name = "fuzz_path" -path = "fuzz_targets/fuzz_path.rs" -test = false -doc = false -bench = false diff --git a/czech-file-knife/fuzz/fuzz_targets/fuzz_path.rs b/czech-file-knife/fuzz/fuzz_targets/fuzz_path.rs deleted file mode 100644 index 4da762fde..000000000 --- a/czech-file-knife/fuzz/fuzz_targets/fuzz_path.rs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Fuzz target for VirtualPath parsing and manipulation - -#![no_main] - -use libfuzzer_sys::fuzz_target; -use cfk_core::path::VirtualPath; - -fuzz_target!(|data: &[u8]| { - // Convert bytes to string for path operations - if let Ok(input) = std::str::from_utf8(data) { - // Fuzz URI parsing - let _ = VirtualPath::parse_uri(input); - - // Fuzz path construction and manipulation - if let Some((backend, path)) = input.split_once('/') { - let vpath = VirtualPath::new(backend, path); - - // Exercise various operations - let _ = vpath.to_uri(); - let _ = vpath.to_path_string(); - let _ = vpath.name(); - let _ = vpath.extension(); - let _ = vpath.parent(); - let _ = vpath.is_root(); - - // Fuzz join with remaining data - if input.len() > 10 { - let _ = vpath.join(&input[..10]); - } - } - } -}); diff --git a/czech-file-knife/guix.scm b/czech-file-knife/guix.scm deleted file mode 100644 index f9142f8a3..000000000 --- a/czech-file-knife/guix.scm +++ /dev/null @@ -1,81 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -;; SPDX-FileCopyrightText: 2025 hyperpolymath -;; -;; Guix package definition for czech-file-knife -;; Build: guix build -f build/guix.scm -;; Shell: guix shell -D -f build/guix.scm - -(use-modules (guix packages) - (guix gexp) - (guix git-download) - (guix build-system cargo) - (guix licenses) - (gnu packages rust) - (gnu packages rust-apps) - (gnu packages pkg-config) - (gnu packages tls) - (gnu packages linux) - (gnu packages databases) - (gnu packages compression)) - -(define-public czech-file-knife - (package - (name "czech-file-knife") - (version "0.1.0") - (source - (local-file "." "czech-file-knife-checkout" - #:recursive? #t - #:select? (git-predicate "."))) - (build-system cargo-build-system) - (arguments - `(#:cargo-build-flags '("-p" "cfk-cli") - #:phases - (modify-phases %standard-phases - (add-after 'install 'install-completions - (lambda* (#:key outputs #:allow-other-keys) - (let* ((out (assoc-ref outputs "out")) - (bash (string-append out "/share/bash-completion/completions")) - (zsh (string-append out "/share/zsh/site-functions")) - (fish (string-append out "/share/fish/vendor_completions.d"))) - (mkdir-p bash) - (mkdir-p zsh) - (mkdir-p fish) - ;; Generate completions via cfk cli - (invoke (string-append out "/bin/cfk") "completion" "bash" - "--output" (string-append bash "/cfk")) - (invoke (string-append out "/bin/cfk") "completion" "zsh" - "--output" (string-append zsh "/_cfk")) - (invoke (string-append out "/bin/cfk") "completion" "fish" - "--output" (string-append fish "/cfk.fish")))))))) - (native-inputs - (list pkg-config rust rust-cargo)) - (inputs - (list openssl - fuse - sqlite)) - (synopsis "Universal cloud file management CLI") - (description - "Czech File Knife (CFK) provides unified access to multiple cloud storage -providers through a single command-line interface. Features include: -@itemize -@item Multi-provider support (S3, GCS, Azure, local) -@item Content-addressable caching with BLAKE3 -@item Full-text search with Tantivy -@item FUSE virtual filesystem mount -@item Provider-agnostic file operations -@end itemize") - (home-page "https://github.com/hyperpolymath/czech-file-knife") - (license agpl3+))) - -;; Workspace development package -(define-public czech-file-knife-dev - (package - (inherit czech-file-knife) - (name "czech-file-knife-dev") - (arguments - `(#:cargo-build-flags '("--workspace"))) - (synopsis "Czech File Knife development package") - (description - "Development package with all workspace crates for czech-file-knife."))) - -czech-file-knife diff --git a/czech-file-knife/hooks/validate-codeql.sh b/czech-file-knife/hooks/validate-codeql.sh deleted file mode 100755 index 15b52c3da..000000000 --- a/czech-file-knife/hooks/validate-codeql.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate CodeQL language matrix matches repo -set -euo pipefail - -CODEQL_FILE=".github/workflows/codeql.yml" -[ -f "$CODEQL_FILE" ] || exit 0 - -# Detect languages in repo -HAS_JS=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" 2>/dev/null | grep -v node_modules | head -1) -HAS_PY=$(find . -name "*.py" 2>/dev/null | grep -v __pycache__ | head -1) -HAS_GO=$(find . -name "*.go" 2>/dev/null | head -1) -HAS_RS=$(find . -name "*.rs" 2>/dev/null | head -1) - -# Check if matrix includes unsupported languages -if grep -q "language:.*python" "$CODEQL_FILE" && [ -z "$HAS_PY" ]; then - echo "WARNING: CodeQL configured for Python but no .py files found" -fi -if grep -q "language:.*go" "$CODEQL_FILE" && [ -z "$HAS_GO" ]; then - echo "WARNING: CodeQL configured for Go but no .go files found" -fi -if grep -q "language:.*javascript" "$CODEQL_FILE" && [ -z "$HAS_JS" ]; then - echo "WARNING: CodeQL configured for JavaScript but no JS/TS files found" -fi - -# Rust/OCaml are not supported - should use 'actions' only -if [ -n "$HAS_RS" ]; then - if grep -q "language:.*rust" "$CODEQL_FILE"; then - echo "ERROR: CodeQL does not support Rust - use ['actions'] instead" - exit 1 - fi -fi - -exit 0 diff --git a/czech-file-knife/hooks/validate-permissions.sh b/czech-file-knife/hooks/validate-permissions.sh deleted file mode 100755 index 1999b018b..000000000 --- a/czech-file-knife/hooks/validate-permissions.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate workflow permissions declarations -set -euo pipefail -ERRORS=0 -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - if ! grep -qE '^permissions:' "$workflow"; then - echo "ERROR: Missing top-level permissions in $workflow" - ERRORS=$((ERRORS + 1)) - fi -done -[ $ERRORS -gt 0 ] && exit 1 -exit 0 diff --git a/czech-file-knife/hooks/validate-sha-pins.sh b/czech-file-knife/hooks/validate-sha-pins.sh deleted file mode 100755 index 697092b52..000000000 --- a/czech-file-knife/hooks/validate-sha-pins.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate GitHub Actions are SHA-pinned - -set -euo pipefail - -ERRORS=0 - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - # Find uses: lines that aren't SHA-pinned - while IFS= read -r line; do - if [[ "$line" =~ uses:.*@ ]]; then - # Check if it has a SHA (40 hex chars) - if ! echo "$line" | grep -qE '@[a-f0-9]{40}'; then - echo "ERROR: Unpinned action in $workflow" - echo " $line" - echo " Actions must use SHA pins: uses: action/name@SHA # version" - ERRORS=$((ERRORS + 1)) - fi - fi - done < "$workflow" -done - -if [ $ERRORS -gt 0 ]; then - echo "" - echo "Found $ERRORS unpinned actions. Please SHA-pin all GitHub Actions." - echo "Use: gh api repos/OWNER/REPO/git/matching-refs/tags/VERSION to find SHAs" - exit 1 -fi - -exit 0 diff --git a/czech-file-knife/hooks/validate-spdx.sh b/czech-file-knife/hooks/validate-spdx.sh deleted file mode 100755 index cc81cf18f..000000000 --- a/czech-file-knife/hooks/validate-spdx.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate SPDX headers in workflow files - -set -euo pipefail - -ERRORS=0 -SPDX_PATTERN="^# SPDX-License-Identifier:" - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - first_line=$(head -n1 "$workflow") - if ! echo "$first_line" | grep -qE "$SPDX_PATTERN"; then - echo "ERROR: Missing SPDX header in $workflow" - echo " First line should be: # SPDX-License-Identifier: MPL-2.0" - ERRORS=$((ERRORS + 1)) - fi -done - -if [ $ERRORS -gt 0 ]; then - exit 1 -fi - -exit 0 diff --git a/czech-file-knife/license/PMPL-1.0.txt b/czech-file-knife/license/PMPL-1.0.txt deleted file mode 100644 index 711e372c1..000000000 --- a/czech-file-knife/license/PMPL-1.0.txt +++ /dev/null @@ -1,162 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL-1.0 LICENSE TEXT --------------------------------------------------------------------------------- - -For exhibits, specifications, provenance rules, and governance: -https://github.com/hyperpolymath/palimpsest-license - -For legal frameworks and jurisdictional analysis: -See /legal/frameworks/ - -For provenance and audit tooling: -See /tools/ and /spec/PROVENANCE-SPEC.adoc - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/czech-file-knife/manifest/sample.ncl b/czech-file-knife/manifest/sample.ncl deleted file mode 100644 index 92a8961e3..000000000 --- a/czech-file-knife/manifest/sample.ncl +++ /dev/null @@ -1,37 +0,0 @@ -{ - meta = { - manifestVersion = "1.0.0", - owner = "Hyperpolymath", - runLabel = "secure-archive-pilot-2025-10", - }, - - providers = [ - { name = "gdrive-main", type = "gdrive", authRef = "secret:gdrive-pilot", root = "root" }, - { name = "dropbox-secure", type = "dropbox", authRef = "secret:dropbox-archive", root = "/secure/archive" } - ], - - selection = { - provider = "gdrive-main", - query = { pathPrefix = "Sensitive_Docs", mimeTypes = ["application/pdf", "text/plain"] } - }, - - flow = { - files = select(selection), - - steps = [ - step "hash" = { plugin = "Hasher", config = { algo = "sha256" } }.map(files), - step "search" = { plugin = "Search", config = { regex = "SSN|CONFIDENTIAL|DOB" } }.map(hash), - step "xfer" = { plugin = "Transfer", config = { from = "gdrive-main", to = "dropbox-secure", mode = "copy" } }.map(search), - step "verify" = { plugin = "Verify", config = { method = "hash", algo = "sha256" } }.map(xfer) - ], - - assertions = [ - assert "hash_present" on hash: "ctx.hash != null" - ] - }, - - policies = { - idempotency = true, - dryRun = true, - } -} diff --git a/czech-file-knife/ops/podman-compose.yml b/czech-file-knife/ops/podman-compose.yml deleted file mode 100644 index c3221df1c..000000000 --- a/czech-file-knife/ops/podman-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "3.9" -# Infrastructure services for Audit (Kafka) and Metadata (Postgres) - -services: - zookeeper: - image: bitnami/zookeeper:latest - environment: - - ALLOW_ANONYMOUS_LOGIN=yes - ports: ["2181:2181"] - - kafka: - image: bitnami/kafka:latest - container_name: filegov-kafka - environment: - - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - - ALLOW_PLAINTEXT_LISTENER=yes - - KAFKA_LISTENERS=PLAINTEXT://:9092 - - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 - ports: ["9092:9092"] - depends_on: ["zookeeper"] - - postgres: - image: postgres:15 - environment: - - POSTGRES_USER=filegov - - POSTGRES_PASSWORD=filegov - - POSTGRES_DB=filegov - ports: ["5432:5432"] diff --git a/czech-file-knife/ops/scripts/ensure_deps.sh b/czech-file-knife/ops/scripts/ensure_deps.sh deleted file mode 100755 index 95c9556b2..000000000 --- a/czech-file-knife/ops/scripts/ensure_deps.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# Checks for all required dependencies (Haskell, Elixir, Bun, Podman, Nickel) - -set -e -echo "[deps] Checking core tools: Bun, Elixir, mix, Stack, Nickel, Podman..." - -# Function to check if a command exists -need() { - command -v "$1" >/dev/null 2>&1 || { - echo "Missing required dependency: $1" >&2 - return 1 - } -} - -MISSING=0 - -# 1. Check core development tools (Bun, Stack, Nickel) -# We check for Bun to manage Svelte/Vite dependencies -for c in bun stack nickel; do - need $c || MISSING=1 -done - -# 2. Check Elixir/Mix (Already installed, but confirms availability) -for c in elixir mix; do - need $c || MISSING=1 -done - -# 3. Check Container Runtime (Flexible: Docker OR Podman) -if (command -v docker >/dev/null 2>&1 && command -v docker-compose >/dev/null 2>&1) || \ - (command -v podman >/dev/null 2>&1 && command -v podman-compose >/dev/null 2>&1); then - echo "[deps] Container runtime (Podman/Docker) OK." -else - echo "Missing container runtime (Docker or Podman) and compose tool." >&2 - MISSING=1 -fi - -if [ $MISSING -eq 1 ]; then - echo "" - echo "--- INSTALLATION GUIDANCE (Remaining) ---" - echo "1. Install **Bun** (for Svelte UI): https://bun.sh/docs/installation (Required instead of Node/NPM)." - echo "2. Install **Haskell Stack** (for the validator): e.g., 'sudo dnf install haskell-stack'." - echo "3. Install **Nickel** (Policy Language): Check official documentation if 'dnf install nickel' failed." - echo "-----------------------------------------" - exit 1 -fi - -echo "[deps] All core tools present. Ready to proceed." diff --git a/czech-file-knife/ops/scripts/ensure_kafka.sh b/czech-file-knife/ops/scripts/ensure_kafka.sh deleted file mode 100755 index af5dab0f3..000000000 --- a/czech-file-knife/ops/scripts/ensure_kafka.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Starts container services and ensures the Kafka topic exists. - -set -e -echo "[kafka] Bringing up Kafka + Zookeeper + ArangoDB for audit..." - -COMPOSE_FILE="ops/podman-compose.yml" - -# Determine which compose tool to use -if command -v podman-compose >/dev/null 2>&1; then - COMPOSE_CMD="podman-compose -f $COMPOSE_FILE" - # Ensure podman service is running if needed (common podman-compose requirement) - # Note: On some systems, this needs to be running or 'podman machine' started. - echo "Attempting to start services using podman-compose..." -else - # Fallback to standard docker compose command - COMPOSE_CMD="docker compose -f $COMPOSE_FILE" - echo "Falling back to standard docker compose..." -fi - -# Start containers -$COMPOSE_CMD up -d - -echo "[kafka] Waiting for Kafka to be ready (5s delay)..." -sleep 5 - -# Create the audit topic (must use the specific container name) -# We use 'podman exec' if podman-compose was used, or 'docker exec' otherwise. -if [[ "$COMPOSE_CMD" == podman-compose* ]]; then - EXEC_CMD="podman exec" -else - EXEC_CMD="docker exec" -fi - -$EXEC_CMD filegov-kafka bash -lc "/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9092 --replication-factor 1 --partitions 1 --topic audit.events.filegov" || true - -echo "[kafka] Kafka infrastructure is ready." diff --git a/czech-file-knife/packaging/arch/PKGBUILD b/czech-file-knife/packaging/arch/PKGBUILD deleted file mode 100644 index d6e6c75e1..000000000 --- a/czech-file-knife/packaging/arch/PKGBUILD +++ /dev/null @@ -1,42 +0,0 @@ -# Maintainer: hyperpolymath -# Arch Linux PKGBUILD for czech-file-knife - -pkgname=czech-file-knife -pkgver=0.1.0 -pkgrel=1 -pkgdesc="Universal file management toolkit with cloud provider integration and virtual filesystem" -arch=('x86_64' 'aarch64') -url="https://github.com/hyperpolymath/czech-file-knife" -license=('AGPL-3.0-or-later') -depends=('gcc-libs' 'fuse3') -makedepends=('rust' 'cargo' 'pkg-config') -optdepends=( - 'tantivy: Full-text search indexing' -) -provides=('cfk') -conflicts=('czech-file-knife-git') -source=("$pkgname-$pkgver.tar.gz::https://github.com/hyperpolymath/czech-file-knife/archive/refs/tags/v$pkgver.tar.gz") -sha256sums=('SKIP') - -build() { - cd "$pkgname-$pkgver" - cargo build --release --package cfk-cli -} - -check() { - cd "$pkgname-$pkgver" - cargo test --release -} - -package() { - cd "$pkgname-$pkgver" - - # Install binary - install -Dm755 "target/release/cfk" "$pkgdir/usr/bin/cfk" - - # Install license - install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" - - # Install documentation - install -Dm644 README.adoc "$pkgdir/usr/share/doc/$pkgname/README.adoc" -} diff --git a/czech-file-knife/packaging/chocolatey/czech-file-knife.nuspec b/czech-file-knife/packaging/chocolatey/czech-file-knife.nuspec deleted file mode 100644 index 558bc4505..000000000 --- a/czech-file-knife/packaging/chocolatey/czech-file-knife.nuspec +++ /dev/null @@ -1,33 +0,0 @@ - - - - - czech-file-knife - 0.1.0 - Czech File Knife - hyperpolymath - hyperpolymath - https://github.com/hyperpolymath/czech-file-knife - https://github.com/hyperpolymath/czech-file-knife/blob/main/LICENSE - false - -Czech File Knife (cfk) is a universal file management toolkit with cloud -provider integration and virtual filesystem support. - -Features: -- Virtual filesystem (FUSE) for mounting cloud storage -- Content-addressable caching with Blake3 hashing -- Full-text search indexing with Tantivy -- Multi-provider support (local, S3, GCS, Azure, etc.) -- Streaming copy with progress indication - - Universal file management toolkit with cloud provider integration - file-manager cloud-storage fuse cli rust - https://github.com/hyperpolymath/czech-file-knife - https://github.com/hyperpolymath/czech-file-knife/blob/main/README.adoc - https://github.com/hyperpolymath/czech-file-knife/issues - - - - - diff --git a/czech-file-knife/packaging/debian/control b/czech-file-knife/packaging/debian/control deleted file mode 100644 index 99832ac1c..000000000 --- a/czech-file-knife/packaging/debian/control +++ /dev/null @@ -1,27 +0,0 @@ -Source: czech-file-knife -Section: utils -Priority: optional -Maintainer: hyperpolymath -Build-Depends: debhelper-compat (= 13), cargo, rustc (>= 1.75), pkg-config, libfuse3-dev -Standards-Version: 4.6.2 -Homepage: https://github.com/hyperpolymath/czech-file-knife -Vcs-Git: https://github.com/hyperpolymath/czech-file-knife.git -Vcs-Browser: https://github.com/hyperpolymath/czech-file-knife -Rules-Requires-Root: no - -Package: czech-file-knife -Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, fuse3 -Description: Universal file management toolkit with cloud provider integration - Czech File Knife (cfk) is a universal file management toolkit with cloud - provider integration and virtual filesystem support. It provides a unified - interface for managing files across local storage and cloud providers. - . - Features: - - Virtual filesystem (FUSE) for mounting cloud storage - - Content-addressable caching with Blake3 hashing - - Full-text search indexing with Tantivy - - Multi-provider support (local, S3, GCS, Azure, etc.) - - Streaming copy with progress indication - . - License: AGPL-3.0-or-later diff --git a/czech-file-knife/packaging/debian/rules b/czech-file-knife/packaging/debian/rules deleted file mode 100644 index 9010ce116..000000000 --- a/czech-file-knife/packaging/debian/rules +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/make -f -# SPDX-License-Identifier: MPL-2.0 - -%: - dh $@ - -override_dh_auto_build: - cargo build --release --package cfk-cli - -override_dh_auto_install: - install -D -m 755 target/release/cfk debian/czech-file-knife/usr/bin/cfk - -override_dh_auto_test: - cargo test --release - -override_dh_auto_clean: - cargo clean diff --git a/czech-file-knife/packaging/flatpak/dev.hyperpolymath.CzechFileKnife.yml b/czech-file-knife/packaging/flatpak/dev.hyperpolymath.CzechFileKnife.yml deleted file mode 100644 index 87afca3ff..000000000 --- a/czech-file-knife/packaging/flatpak/dev.hyperpolymath.CzechFileKnife.yml +++ /dev/null @@ -1,31 +0,0 @@ -app-id: dev.hyperpolymath.CzechFileKnife -runtime: org.freedesktop.Platform -runtime-version: '23.08' -sdk: org.freedesktop.Sdk -sdk-extensions: - - org.freedesktop.Sdk.Extension.rust-stable - -command: cfk - -finish-args: - - --filesystem=home - - --filesystem=/mnt - - --filesystem=/run/media - - --device=fuse - -build-options: - append-path: /usr/lib/sdk/rust-stable/bin - env: - CARGO_HOME: /run/build/czech-file-knife/cargo - RUSTUP_HOME: /usr/lib/sdk/rust-stable - -modules: - - name: czech-file-knife - buildsystem: simple - build-commands: - - cargo build --release --package cfk-cli - - install -Dm755 target/release/cfk /app/bin/cfk - sources: - - type: git - url: https://github.com/hyperpolymath/czech-file-knife.git - tag: v0.1.0 diff --git a/czech-file-knife/packaging/macports/Portfile b/czech-file-knife/packaging/macports/Portfile deleted file mode 100644 index 079c375f9..000000000 --- a/czech-file-knife/packaging/macports/Portfile +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 -# SPDX-License-Identifier: MPL-2.0 - -PortSystem 1.0 -PortGroup cargo 1.0 -PortGroup github 1.0 - -github.setup hyperpolymath czech-file-knife 0.1.0 v -revision 0 -categories sysutils -license AGPL-3.0-or-later -maintainers {hyperpolymath @hyperpolymath} -description Universal file management toolkit with cloud provider integration -long_description Czech File Knife (cfk) is a universal file management toolkit with \ - cloud provider integration and virtual filesystem support. It provides \ - a unified interface for managing files across local storage and cloud \ - providers including S3, GCS, Azure, and more. - -homepage https://github.com/hyperpolymath/czech-file-knife - -checksums rmd160 SKIP \ - sha256 SKIP \ - size SKIP - -depends_lib-append port:macfuse - -cargo.crates # Will be auto-populated by cargo2port - -destroot { - xinstall -m 755 ${worksrcpath}/target/release/cfk ${destroot}${prefix}/bin/ -} - -notes " -Czech File Knife requires macFUSE to be installed for virtual filesystem features. -Install with: sudo port install macfuse -" diff --git a/czech-file-knife/packaging/rpm/czech-file-knife.spec b/czech-file-knife/packaging/rpm/czech-file-knife.spec deleted file mode 100644 index 32eff56dc..000000000 --- a/czech-file-knife/packaging/rpm/czech-file-knife.spec +++ /dev/null @@ -1,52 +0,0 @@ -# RPM spec file for czech-file-knife -# Compatible with Fedora (dnf) and openSUSE (zypper) - -Name: czech-file-knife -Version: 0.1.0 -Release: 1%{?dist} -Summary: Universal file management toolkit with cloud provider integration - -License: AGPL-3.0-or-later -URL: https://github.com/hyperpolymath/czech-file-knife -Source0: %{name}-%{version}.tar.gz - -BuildRequires: rust >= 1.75 -BuildRequires: cargo -BuildRequires: gcc -BuildRequires: pkg-config -BuildRequires: fuse3-devel - -Requires: fuse3 - -%description -Czech File Knife (cfk) is a universal file management toolkit with cloud -provider integration and virtual filesystem support. It provides a unified -interface for managing files across local storage and cloud providers. - -Features: -- Virtual filesystem (FUSE) for mounting cloud storage -- Content-addressable caching with Blake3 hashing -- Full-text search indexing with Tantivy -- Multi-provider support (local, S3, GCS, Azure, etc.) -- Streaming copy with progress indication - -%prep -%autosetup - -%build -cargo build --release --package cfk-cli - -%check -cargo test --release - -%install -install -D -m 755 target/release/cfk %{buildroot}%{_bindir}/cfk - -%files -%license LICENSE -%doc README.adoc -%{_bindir}/cfk - -%changelog -* Tue Dec 17 2025 hyperpolymath - 0.1.0-1 -- Initial release diff --git a/czech-file-knife/packaging/scoop/czech-file-knife.json b/czech-file-knife/packaging/scoop/czech-file-knife.json deleted file mode 100644 index b68f05f56..000000000 --- a/czech-file-knife/packaging/scoop/czech-file-knife.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "0.1.0", - "description": "Universal file management toolkit with cloud provider integration and virtual filesystem", - "homepage": "https://github.com/hyperpolymath/czech-file-knife", - "license": "AGPL-3.0-or-later", - "architecture": { - "64bit": { - "url": "https://github.com/hyperpolymath/czech-file-knife/releases/download/v0.1.0/cfk-0.1.0-x86_64-pc-windows-msvc.zip", - "hash": "TODO", - "bin": "cfk.exe" - } - }, - "checkver": { - "github": "https://github.com/hyperpolymath/czech-file-knife" - }, - "autoupdate": { - "architecture": { - "64bit": { - "url": "https://github.com/hyperpolymath/czech-file-knife/releases/download/v$version/cfk-$version-x86_64-pc-windows-msvc.zip" - } - } - } -} diff --git a/czech-file-knife/packaging/winget/czech-file-knife.yaml b/czech-file-knife/packaging/winget/czech-file-knife.yaml deleted file mode 100644 index 32f9751ae..000000000 --- a/czech-file-knife/packaging/winget/czech-file-knife.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# winget manifest for czech-file-knife -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.4.0.schema.json - -PackageIdentifier: Hyperpolymath.CzechFileKnife -PackageVersion: 0.1.0 -PackageLocale: en-US -Publisher: hyperpolymath -PublisherUrl: https://github.com/hyperpolymath -PackageName: Czech File Knife -PackageUrl: https://github.com/hyperpolymath/czech-file-knife -License: AGPL-3.0-or-later -LicenseUrl: https://github.com/hyperpolymath/czech-file-knife/blob/main/LICENSE -ShortDescription: Universal file management toolkit with cloud provider integration -Description: | - Czech File Knife (cfk) is a universal file management toolkit with cloud - provider integration and virtual filesystem support. It provides a unified - interface for managing files across local storage and cloud providers. - - Features: - - Virtual filesystem (FUSE) for mounting cloud storage - - Content-addressable caching with Blake3 hashing - - Full-text search indexing with Tantivy - - Multi-provider support (local, S3, GCS, Azure, etc.) - - Streaming copy with progress indication -Tags: - - file-manager - - cloud-storage - - fuse - - cli - - rust -Moniker: cfk -Commands: - - cfk -Installers: - - Architecture: x64 - InstallerType: zip - InstallerUrl: https://github.com/hyperpolymath/czech-file-knife/releases/download/v0.1.0/cfk-0.1.0-x86_64-pc-windows-msvc.zip - InstallerSha256: TODO -ManifestType: singleton -ManifestVersion: 1.4.0