From 3b6e9566bc8b71031f16aa4c7187ad57e287b2f5 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:31:50 +0100 Subject: [PATCH] chore: remove czech-file-knife/, now its own repo The czech-file-knife product (the cfk-* Rust workspace, its Ada TUI, packaging, ops and docs) was developed here before hyperpolymath/czech-file-knife existed. It landed there verbatim from this repo's 70f4e1b8 in hyperpolymath/czech-file-knife#3 (squash 85506b8b), by the owner's 2026-10-09 ruling to extract it to a standalone repo. Removed: the 164 tracked files under czech-file-knife/. References updated so nothing points at the deleted tree: - .github/dependabot.yml: drop the /czech-file-knife cargo directory. - .hypatia-baseline.json: drop the 55 entries keyed on czech-file-knife/ paths (214 -> 159). Deleting baseline entries is debt reduction; no other entry changes (a jq round-trip of the base file is byte-identical). - stapeln.toml: drop the cfk-build and cfk-runtime layers and their four target references; the file-tools target keeps panoptes and displace. - TOPOLOGY.adoc, TEST-NEEDS.adoc, llm-warmup-dev.adoc: drop the czech-file-knife rows; the warm-up notes where it went. Left as they are: audits/assail-classifications.a2ml and audits/audit-ffi-2026-05-26.adoc are dated records of what was audited then, and .hypatia/scan-cache/fix-todo-markers.hashes is a cache last written 2026-03-14. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BbkZgW7JLugsZ5MhRjF8iE --- .github/dependabot.yml | 1 - .hypatia-baseline.json | 330 - TEST-NEEDS.adoc | 1 - TOPOLOGY.adoc | 1 - czech-file-knife/.claude/CLAUDE.md | 83 - .../.clusterfuzzlite/Containerfile | 8 - czech-file-knife/.clusterfuzzlite/build.sh | 13 - .../.clusterfuzzlite/project.yaml | 3 - czech-file-knife/.editorconfig | 68 - czech-file-knife/.gitattributes | 54 - czech-file-knife/.github/FUNDING.yml | 7 - .../.github/ISSUE_TEMPLATE/bug_report.md | 38 - .../.github/ISSUE_TEMPLATE/custom.md | 10 - .../.github/ISSUE_TEMPLATE/documentation.md | 66 - .../.github/ISSUE_TEMPLATE/feature_request.md | 20 - .../.github/ISSUE_TEMPLATE/question.md | 55 - czech-file-knife/.github/dependabot.yml | 28 - .../.github/workflows/cflite_batch.yml | 32 - .../.github/workflows/cflite_pr.yml | 31 - czech-file-knife/.github/workflows/codeql.yml | 40 - .../.github/workflows/container.yml | 56 - .../.github/workflows/ghcr-publish.yml | 65 - .../.github/workflows/guix-nix-policy.yml | 35 - .../.github/workflows/hypatia-scan.yml | 179 - .../.github/workflows/instant-sync.yml | 33 - czech-file-knife/.github/workflows/mirror.yml | 144 - .../.github/workflows/npm-bun-blocker.yml | 20 - .../.github/workflows/publish.yml | 152 - .../.github/workflows/quality.yml | 58 - .../.github/workflows/release.yml | 180 - .../.github/workflows/scorecard-enforcer.yml | 57 - .../.github/workflows/scorecard.yml | 32 - .../.github/workflows/secret-scanner.yml | 67 - .../.github/workflows/security-policy.yml | 43 - .../.github/workflows/stress-test.yml | 54 - .../.github/workflows/ts-blocker.yml | 25 - .../workflows/wellknown-enforcement.yml | 96 - .../.github/workflows/workflow-linter.yml | 156 - czech-file-knife/.gitignore | 90 - czech-file-knife/.gitlab-ci.yml | 173 - .../.machine_readable/6a2/AGENTIC.a2ml | 39 - .../.machine_readable/6a2/ECOSYSTEM.a2ml | 20 - .../.machine_readable/6a2/META.a2ml | 27 - .../.machine_readable/6a2/NEUROSYM.a2ml | 21 - .../.machine_readable/6a2/PLAYBOOK.a2ml | 26 - .../.machine_readable/6a2/STATE.a2ml | 38 - czech-file-knife/.nojekyll | 0 czech-file-knife/.tool-versions | 5 - czech-file-knife/ABI-FFI-README.adoc | 407 - czech-file-knife/CODE_OF_CONDUCT.adoc | 39 - czech-file-knife/CONTRIBUTING.adoc | 108 - czech-file-knife/Cargo.lock | 7120 ----------------- czech-file-knife/Cargo.toml | 100 - czech-file-knife/Containerfile | 38 - czech-file-knife/Justfile | 29 - czech-file-knife/LICENSE | 153 - czech-file-knife/MAINTAINERS.adoc | 47 - czech-file-knife/Mustfile | 13 - czech-file-knife/PALIMPSEST.adoc | 41 - czech-file-knife/README.adoc | 219 - czech-file-knife/ROADMAP.adoc | 171 - czech-file-knife/RSR_OUTLINE.adoc | 218 - czech-file-knife/SECURITY.adoc | 24 - czech-file-knife/TESTING-REPORT.adoc | 84 - czech-file-knife/TESTING-REPORT.scm | 65 - .../benches/czech_file_knife_bench.rs | 14 - czech-file-knife/cfk-cache/Cargo.toml | 38 - czech-file-knife/cfk-cache/src/blob_store.rs | 449 -- czech-file-knife/cfk-cache/src/lib.rs | 178 - .../cfk-cache/src/metadata_cache.rs | 534 -- czech-file-knife/cfk-cache/src/policy.rs | 423 - .../cfk-cache/src/sled_backend.rs | 76 - czech-file-knife/cfk-cli/Cargo.toml | 59 - czech-file-knife/cfk-cli/src/commands.rs | 401 - czech-file-knife/cfk-cli/src/main.rs | 166 - czech-file-knife/cfk-core/Cargo.toml | 15 - czech-file-knife/cfk-core/src/backend.rs | 123 - czech-file-knife/cfk-core/src/entry.rs | 116 - czech-file-knife/cfk-core/src/error.rs | 147 - czech-file-knife/cfk-core/src/lib.rs | 18 - czech-file-knife/cfk-core/src/metadata.rs | 60 - czech-file-knife/cfk-core/src/operations.rs | 41 - czech-file-knife/cfk-core/src/path.rs | 234 - czech-file-knife/cfk-core/src/platform.rs | 219 - czech-file-knife/cfk-integrations/Cargo.toml | 26 - .../cfk-integrations/src/agrep.rs | 101 - .../cfk-integrations/src/aria2.rs | 76 - czech-file-knife/cfk-integrations/src/lib.rs | 64 - .../cfk-integrations/src/pandoc.rs | 110 - czech-file-knife/cfk-ios/Cargo.toml | 52 - czech-file-knife/cfk-ios/src/domain.rs | 194 - czech-file-knife/cfk-ios/src/error.rs | 139 - czech-file-knife/cfk-ios/src/ffi.rs | 474 -- czech-file-knife/cfk-ios/src/item.rs | 342 - czech-file-knife/cfk-ios/src/lib.rs | 91 - czech-file-knife/cfk-ios/src/provider.rs | 499 -- czech-file-knife/cfk-ios/swift/CfkBridge.h | 239 - .../swift/CfkFileProviderExtension.swift | 337 - .../cfk-ios/swift/CfkFileProviderItem.swift | 243 - czech-file-knife/cfk-providers/Cargo.toml | 45 - czech-file-knife/cfk-providers/src/afs.rs | 530 -- czech-file-knife/cfk-providers/src/box_com.rs | 721 -- czech-file-knife/cfk-providers/src/ceph.rs | 454 -- czech-file-knife/cfk-providers/src/dropbox.rs | 582 -- czech-file-knife/cfk-providers/src/gdrive.rs | 731 -- czech-file-knife/cfk-providers/src/ipfs.rs | 802 -- czech-file-knife/cfk-providers/src/lib.rs | 137 - czech-file-knife/cfk-providers/src/local.rs | 484 -- czech-file-knife/cfk-providers/src/nfs.rs | 589 -- czech-file-knife/cfk-providers/src/ninep.rs | 960 --- .../cfk-providers/src/onedrive.rs | 620 -- .../cfk-providers/src/protocols.rs | 763 -- czech-file-knife/cfk-providers/src/s3.rs | 711 -- czech-file-knife/cfk-providers/src/sftp.rs | 604 -- czech-file-knife/cfk-providers/src/smb.rs | 781 -- .../cfk-providers/src/syncthing.rs | 531 -- .../cfk-providers/src/transport.rs | 593 -- czech-file-knife/cfk-providers/src/webdav.rs | 611 -- czech-file-knife/cfk-search/Cargo.toml | 30 - czech-file-knife/cfk-search/src/lib.rs | 648 -- czech-file-knife/cfk-tui/cfk_tui.gpr | 38 - .../cfk-tui/src/cfk-tui-application.ads | 39 - czech-file-knife/cfk-tui/src/cfk_tui_main.adb | 36 - czech-file-knife/cfk-vfs/Cargo.toml | 29 - czech-file-knife/cfk-vfs/src/lib.rs | 471 -- czech-file-knife/contractiles/README.adoc | 19 - czech-file-knife/contractiles/dust/Dustfile | 29 - czech-file-knife/contractiles/k9/README.adoc | 178 - .../contractiles/k9/examples/ci-config.k9.ncl | 126 - .../k9/examples/project-metadata.k9.ncl | 57 - .../k9/examples/setup-repo.k9.ncl | 167 - .../contractiles/k9/template-hunt.k9.ncl | 136 - .../contractiles/k9/template-kennel.k9.ncl | 54 - .../contractiles/k9/template-yard.k9.ncl | 84 - czech-file-knife/contractiles/must/Mustfile | 35 - czech-file-knife/deny.toml | 60 - czech-file-knife/docs/CITATIONS.adoc | 36 - .../docs/DISTRIBUTED_FILESYSTEMS.adoc | 380 - czech-file-knife/docs/IOS_INTEGRATION.adoc | 394 - czech-file-knife/eclexiaiser.toml | 26 - .../examples/web-project-deno.json | 20 - czech-file-knife/ffi/zig/build.zig | 94 - czech-file-knife/ffi/zig/src/main.zig | 274 - .../ffi/zig/test/integration_test.zig | 182 - czech-file-knife/fuzz/Cargo.toml | 24 - .../fuzz/fuzz_targets/fuzz_path.rs | 33 - czech-file-knife/guix.scm | 81 - czech-file-knife/hooks/validate-codeql.sh | 34 - .../hooks/validate-permissions.sh | 14 - czech-file-knife/hooks/validate-sha-pins.sh | 33 - czech-file-knife/hooks/validate-spdx.sh | 25 - czech-file-knife/license/PMPL-1.0.txt | 162 - czech-file-knife/manifest/sample.ncl | 37 - czech-file-knife/ops/podman-compose.yml | 28 - czech-file-knife/ops/scripts/ensure_deps.sh | 47 - czech-file-knife/ops/scripts/ensure_kafka.sh | 37 - czech-file-knife/packaging/arch/PKGBUILD | 42 - .../chocolatey/czech-file-knife.nuspec | 33 - czech-file-knife/packaging/debian/control | 27 - czech-file-knife/packaging/debian/rules | 17 - .../dev.hyperpolymath.CzechFileKnife.yml | 31 - czech-file-knife/packaging/macports/Portfile | 36 - .../packaging/rpm/czech-file-knife.spec | 52 - .../packaging/scoop/czech-file-knife.json | 23 - .../packaging/winget/czech-file-knife.yaml | 40 - czech-file-knife/src/abi/Foreign.idr | 216 - czech-file-knife/src/abi/Layout.idr | 176 - czech-file-knife/src/abi/Types.idr | 231 - llm-warmup-dev.adoc | 6 +- stapeln.toml | 33 +- 170 files changed, 3 insertions(+), 34360 deletions(-) delete mode 100644 czech-file-knife/.claude/CLAUDE.md delete mode 100644 czech-file-knife/.clusterfuzzlite/Containerfile delete mode 100644 czech-file-knife/.clusterfuzzlite/build.sh delete mode 100644 czech-file-knife/.clusterfuzzlite/project.yaml delete mode 100644 czech-file-knife/.editorconfig delete mode 100644 czech-file-knife/.gitattributes delete mode 100644 czech-file-knife/.github/FUNDING.yml delete mode 100644 czech-file-knife/.github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 czech-file-knife/.github/ISSUE_TEMPLATE/custom.md delete mode 100644 czech-file-knife/.github/ISSUE_TEMPLATE/documentation.md delete mode 100644 czech-file-knife/.github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 czech-file-knife/.github/ISSUE_TEMPLATE/question.md delete mode 100644 czech-file-knife/.github/dependabot.yml delete mode 100644 czech-file-knife/.github/workflows/cflite_batch.yml delete mode 100644 czech-file-knife/.github/workflows/cflite_pr.yml delete mode 100644 czech-file-knife/.github/workflows/codeql.yml delete mode 100644 czech-file-knife/.github/workflows/container.yml delete mode 100644 czech-file-knife/.github/workflows/ghcr-publish.yml delete mode 100644 czech-file-knife/.github/workflows/guix-nix-policy.yml delete mode 100644 czech-file-knife/.github/workflows/hypatia-scan.yml delete mode 100644 czech-file-knife/.github/workflows/instant-sync.yml delete mode 100644 czech-file-knife/.github/workflows/mirror.yml delete mode 100644 czech-file-knife/.github/workflows/npm-bun-blocker.yml delete mode 100644 czech-file-knife/.github/workflows/publish.yml delete mode 100644 czech-file-knife/.github/workflows/quality.yml delete mode 100644 czech-file-knife/.github/workflows/release.yml delete mode 100644 czech-file-knife/.github/workflows/scorecard-enforcer.yml delete mode 100644 czech-file-knife/.github/workflows/scorecard.yml delete mode 100644 czech-file-knife/.github/workflows/secret-scanner.yml delete mode 100644 czech-file-knife/.github/workflows/security-policy.yml delete mode 100644 czech-file-knife/.github/workflows/stress-test.yml delete mode 100644 czech-file-knife/.github/workflows/ts-blocker.yml delete mode 100644 czech-file-knife/.github/workflows/wellknown-enforcement.yml delete mode 100644 czech-file-knife/.github/workflows/workflow-linter.yml delete mode 100644 czech-file-knife/.gitignore delete mode 100644 czech-file-knife/.gitlab-ci.yml delete mode 100644 czech-file-knife/.machine_readable/6a2/AGENTIC.a2ml delete mode 100644 czech-file-knife/.machine_readable/6a2/ECOSYSTEM.a2ml delete mode 100644 czech-file-knife/.machine_readable/6a2/META.a2ml delete mode 100644 czech-file-knife/.machine_readable/6a2/NEUROSYM.a2ml delete mode 100644 czech-file-knife/.machine_readable/6a2/PLAYBOOK.a2ml delete mode 100644 czech-file-knife/.machine_readable/6a2/STATE.a2ml delete mode 100644 czech-file-knife/.nojekyll delete mode 100644 czech-file-knife/.tool-versions delete mode 100644 czech-file-knife/ABI-FFI-README.adoc delete mode 100644 czech-file-knife/CODE_OF_CONDUCT.adoc delete mode 100644 czech-file-knife/CONTRIBUTING.adoc delete mode 100644 czech-file-knife/Cargo.lock delete mode 100644 czech-file-knife/Cargo.toml delete mode 100644 czech-file-knife/Containerfile delete mode 100644 czech-file-knife/Justfile delete mode 100644 czech-file-knife/LICENSE delete mode 100644 czech-file-knife/MAINTAINERS.adoc delete mode 100644 czech-file-knife/Mustfile delete mode 100644 czech-file-knife/PALIMPSEST.adoc delete mode 100644 czech-file-knife/README.adoc delete mode 100644 czech-file-knife/ROADMAP.adoc delete mode 100644 czech-file-knife/RSR_OUTLINE.adoc delete mode 100644 czech-file-knife/SECURITY.adoc delete mode 100644 czech-file-knife/TESTING-REPORT.adoc delete mode 100644 czech-file-knife/TESTING-REPORT.scm delete mode 100644 czech-file-knife/benches/czech_file_knife_bench.rs delete mode 100644 czech-file-knife/cfk-cache/Cargo.toml delete mode 100644 czech-file-knife/cfk-cache/src/blob_store.rs delete mode 100644 czech-file-knife/cfk-cache/src/lib.rs delete mode 100644 czech-file-knife/cfk-cache/src/metadata_cache.rs delete mode 100644 czech-file-knife/cfk-cache/src/policy.rs delete mode 100644 czech-file-knife/cfk-cache/src/sled_backend.rs delete mode 100644 czech-file-knife/cfk-cli/Cargo.toml delete mode 100644 czech-file-knife/cfk-cli/src/commands.rs delete mode 100644 czech-file-knife/cfk-cli/src/main.rs delete mode 100644 czech-file-knife/cfk-core/Cargo.toml delete mode 100644 czech-file-knife/cfk-core/src/backend.rs delete mode 100644 czech-file-knife/cfk-core/src/entry.rs delete mode 100644 czech-file-knife/cfk-core/src/error.rs delete mode 100644 czech-file-knife/cfk-core/src/lib.rs delete mode 100644 czech-file-knife/cfk-core/src/metadata.rs delete mode 100644 czech-file-knife/cfk-core/src/operations.rs delete mode 100644 czech-file-knife/cfk-core/src/path.rs delete mode 100644 czech-file-knife/cfk-core/src/platform.rs delete mode 100644 czech-file-knife/cfk-integrations/Cargo.toml delete mode 100644 czech-file-knife/cfk-integrations/src/agrep.rs delete mode 100644 czech-file-knife/cfk-integrations/src/aria2.rs delete mode 100644 czech-file-knife/cfk-integrations/src/lib.rs delete mode 100644 czech-file-knife/cfk-integrations/src/pandoc.rs delete mode 100644 czech-file-knife/cfk-ios/Cargo.toml delete mode 100644 czech-file-knife/cfk-ios/src/domain.rs delete mode 100644 czech-file-knife/cfk-ios/src/error.rs delete mode 100644 czech-file-knife/cfk-ios/src/ffi.rs delete mode 100644 czech-file-knife/cfk-ios/src/item.rs delete mode 100644 czech-file-knife/cfk-ios/src/lib.rs delete mode 100644 czech-file-knife/cfk-ios/src/provider.rs delete mode 100644 czech-file-knife/cfk-ios/swift/CfkBridge.h delete mode 100644 czech-file-knife/cfk-ios/swift/CfkFileProviderExtension.swift delete mode 100644 czech-file-knife/cfk-ios/swift/CfkFileProviderItem.swift delete mode 100644 czech-file-knife/cfk-providers/Cargo.toml delete mode 100644 czech-file-knife/cfk-providers/src/afs.rs delete mode 100644 czech-file-knife/cfk-providers/src/box_com.rs delete mode 100644 czech-file-knife/cfk-providers/src/ceph.rs delete mode 100644 czech-file-knife/cfk-providers/src/dropbox.rs delete mode 100644 czech-file-knife/cfk-providers/src/gdrive.rs delete mode 100644 czech-file-knife/cfk-providers/src/ipfs.rs delete mode 100644 czech-file-knife/cfk-providers/src/lib.rs delete mode 100644 czech-file-knife/cfk-providers/src/local.rs delete mode 100644 czech-file-knife/cfk-providers/src/nfs.rs delete mode 100644 czech-file-knife/cfk-providers/src/ninep.rs delete mode 100644 czech-file-knife/cfk-providers/src/onedrive.rs delete mode 100644 czech-file-knife/cfk-providers/src/protocols.rs delete mode 100644 czech-file-knife/cfk-providers/src/s3.rs delete mode 100644 czech-file-knife/cfk-providers/src/sftp.rs delete mode 100644 czech-file-knife/cfk-providers/src/smb.rs delete mode 100644 czech-file-knife/cfk-providers/src/syncthing.rs delete mode 100644 czech-file-knife/cfk-providers/src/transport.rs delete mode 100644 czech-file-knife/cfk-providers/src/webdav.rs delete mode 100644 czech-file-knife/cfk-search/Cargo.toml delete mode 100644 czech-file-knife/cfk-search/src/lib.rs delete mode 100644 czech-file-knife/cfk-tui/cfk_tui.gpr delete mode 100644 czech-file-knife/cfk-tui/src/cfk-tui-application.ads delete mode 100644 czech-file-knife/cfk-tui/src/cfk_tui_main.adb delete mode 100644 czech-file-knife/cfk-vfs/Cargo.toml delete mode 100644 czech-file-knife/cfk-vfs/src/lib.rs delete mode 100644 czech-file-knife/contractiles/README.adoc delete mode 100644 czech-file-knife/contractiles/dust/Dustfile delete mode 100644 czech-file-knife/contractiles/k9/README.adoc delete mode 100644 czech-file-knife/contractiles/k9/examples/ci-config.k9.ncl delete mode 100644 czech-file-knife/contractiles/k9/examples/project-metadata.k9.ncl delete mode 100644 czech-file-knife/contractiles/k9/examples/setup-repo.k9.ncl delete mode 100644 czech-file-knife/contractiles/k9/template-hunt.k9.ncl delete mode 100644 czech-file-knife/contractiles/k9/template-kennel.k9.ncl delete mode 100644 czech-file-knife/contractiles/k9/template-yard.k9.ncl delete mode 100644 czech-file-knife/contractiles/must/Mustfile delete mode 100644 czech-file-knife/deny.toml delete mode 100644 czech-file-knife/docs/CITATIONS.adoc delete mode 100644 czech-file-knife/docs/DISTRIBUTED_FILESYSTEMS.adoc delete mode 100644 czech-file-knife/docs/IOS_INTEGRATION.adoc delete mode 100644 czech-file-knife/eclexiaiser.toml delete mode 100644 czech-file-knife/examples/web-project-deno.json delete mode 100644 czech-file-knife/ffi/zig/build.zig delete mode 100644 czech-file-knife/ffi/zig/src/main.zig delete mode 100644 czech-file-knife/ffi/zig/test/integration_test.zig delete mode 100644 czech-file-knife/fuzz/Cargo.toml delete mode 100644 czech-file-knife/fuzz/fuzz_targets/fuzz_path.rs delete mode 100644 czech-file-knife/guix.scm delete mode 100755 czech-file-knife/hooks/validate-codeql.sh delete mode 100755 czech-file-knife/hooks/validate-permissions.sh delete mode 100755 czech-file-knife/hooks/validate-sha-pins.sh delete mode 100755 czech-file-knife/hooks/validate-spdx.sh delete mode 100644 czech-file-knife/license/PMPL-1.0.txt delete mode 100644 czech-file-knife/manifest/sample.ncl delete mode 100644 czech-file-knife/ops/podman-compose.yml delete mode 100755 czech-file-knife/ops/scripts/ensure_deps.sh delete mode 100755 czech-file-knife/ops/scripts/ensure_kafka.sh delete mode 100644 czech-file-knife/packaging/arch/PKGBUILD delete mode 100644 czech-file-knife/packaging/chocolatey/czech-file-knife.nuspec delete mode 100644 czech-file-knife/packaging/debian/control delete mode 100644 czech-file-knife/packaging/debian/rules delete mode 100644 czech-file-knife/packaging/flatpak/dev.hyperpolymath.CzechFileKnife.yml delete mode 100644 czech-file-knife/packaging/macports/Portfile delete mode 100644 czech-file-knife/packaging/rpm/czech-file-knife.spec delete mode 100644 czech-file-knife/packaging/scoop/czech-file-knife.json delete mode 100644 czech-file-knife/packaging/winget/czech-file-knife.yaml delete mode 100644 czech-file-knife/src/abi/Foreign.idr delete mode 100644 czech-file-knife/src/abi/Layout.idr delete mode 100644 czech-file-knife/src/abi/Types.idr diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d376d2f..2553f172 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,7 +19,6 @@ updates: - package-ecosystem: "cargo" directories: - "/" - - "/czech-file-knife" - "/panoptes" - "/personal-sysadmin" - "/displace" diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json index 241ae150..bfd0e259 100644 --- a/.hypatia-baseline.json +++ b/.hypatia-baseline.json @@ -197,12 +197,6 @@ "type": "rescript_file_present", "file": "composer/examples/SafeDOMExample.res" }, - { - "severity": "critical", - "rule_module": "banned_language", - "type": "rescript_file_present", - "file": "czech-file-knife/examples/SafeDOMExample.res" - }, { "severity": "critical", "rule_module": "banned_language", @@ -329,90 +323,6 @@ "type": "unwrap_dangerous_default", "file": "clinician/src/tools/network.rs" }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-cache/src/metadata_cache.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-integrations/src/agrep.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-ios/src/ffi.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-ios/src/provider.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/afs.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/box_com.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/dropbox.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/gdrive.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/ipfs.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/onedrive.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/protocols.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/s3.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-providers/src/webdav.rs" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "czech-file-knife/cfk-search/src/lib.rs" - }, { "severity": "critical", "rule_module": "code_safety", @@ -479,12 +389,6 @@ "type": "secret_detected", "file": "cicada/scripts/verify_rsr.jl" }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "czech-file-knife/docs/DISTRIBUTED_FILESYSTEMS.md" - }, { "severity": "critical", "rule_module": "security_errors", @@ -545,30 +449,12 @@ "type": "missing_requirement", "file": "permissions: read-all" }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "from_raw", - "file": "czech-file-knife/cfk-ios/src/error.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "from_raw", - "file": "czech-file-knife/cfk-ios/src/ffi.rs" - }, { "severity": "high", "rule_module": "code_safety", "type": "from_raw", "file": "port-endoscope/src/process.rs" }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "mem_forget", - "file": "czech-file-knife/cfk-ios/src/ffi.rs" - }, { "severity": "high", "rule_module": "code_safety", @@ -755,114 +641,6 @@ "type": "unwrap_without_check", "file": "contracts-rust/tests/contract_tests.rs" }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-cache/src/blob_store.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-cache/src/metadata_cache.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-core/src/path.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/agrep.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/aria2.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/pandoc.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/afs.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/box_com.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/dropbox.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/gdrive.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/local.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/nfs.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/onedrive.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/syncthing.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/transport.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/webdav.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-search/src/lib.rs" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-vfs/src/lib.rs" - }, { "severity": "high", "rule_module": "code_safety", @@ -1127,114 +905,6 @@ "type": "unwrap_without_check", "file": "contracts-rust/tests/contract_tests.rs" }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-cache/src/blob_store.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-cache/src/metadata_cache.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-core/src/path.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/agrep.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/aria2.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-integrations/src/pandoc.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/afs.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/box_com.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/dropbox.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/gdrive.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/local.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/nfs.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/onedrive.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/syncthing.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/transport.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-providers/src/webdav.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-search/src/lib.rs" - }, - { - "severity": "high", - "rule_module": "unsafe_panic", - "type": "unwrap_without_check", - "file": "czech-file-knife/cfk-vfs/src/lib.rs" - }, { "severity": "high", "rule_module": "unsafe_panic", diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index df91fe41..75070238 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -38,7 +38,6 @@ This is a monorepo with 20+ components. Coverage is extremely uneven: * *clinician/* (Rust) — Cargo.toml exists, 0 test files * *hardware-crash-team/* (Rust) — Cargo.toml exists, 0 test files * *contracts-rust/* (Rust) — Cargo.toml exists, 0 test files -* *czech-file-knife/* (Rust) — bench file exists but 0 test files * *displace/* — no tests * *emergency-button/* — no tests * *emergency-room/* — no tests diff --git a/TOPOLOGY.adoc b/TOPOLOGY.adoc index eb97894c..db357a35 100644 --- a/TOPOLOGY.adoc +++ b/TOPOLOGY.adoc @@ -71,7 +71,6 @@ DATA & CONTRACTS SYSTEM TOOLS (ambientops monorepo members) broad-spectrum (AffineScript) ██████░░░░ 60% Link/SEO/accessibility checker cicada ██░░░░░░░░ 20% Stub - czech-file-knife ██░░░░░░░░ 20% File utilities displace ██░░░░░░░░ 20% Displacement tool emergency-button ██░░░░░░░░ 20% Emergency stop hybrid-automation-router ██░░░░░░░░ 20% HAR integration diff --git a/czech-file-knife/.claude/CLAUDE.md b/czech-file-knife/.claude/CLAUDE.md deleted file mode 100644 index 467ab4c7..00000000 --- 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 | Compiles to JS, type-safe | -| **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**: Guix (flake.guix) -- **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/Containerfile b/czech-file-knife/.clusterfuzzlite/Containerfile deleted file mode 100644 index bc3c04ea..00000000 --- a/czech-file-knife/.clusterfuzzlite/Containerfile +++ /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 94310684..00000000 --- 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 77e9a190..00000000 --- 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 f6757374..00000000 --- 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 e860a85c..00000000 --- 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 688a442c..00000000 --- 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 987aab6b..00000000 --- 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 48d5f81f..00000000 --- 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 4fcb9f9f..00000000 --- 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 3e8fa7e7..00000000 --- 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 fd0e2a5c..00000000 --- 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 72c20975..00000000 --- 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 23e06414..00000000 --- a/czech-file-knife/.github/workflows/cflite_batch.yml +++ /dev/null @@ -1,32 +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 92a93813..00000000 --- a/czech-file-knife/.github/workflows/cflite_pr.yml +++ /dev/null @@ -1,31 +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 6e603845..00000000 --- a/czech-file-knife/.github/workflows/codeql.yml +++ /dev/null @@ -1,40 +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 337c9c9c..00000000 --- a/czech-file-knife/.github/workflows/container.yml +++ /dev/null @@ -1,56 +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 6ce37456..00000000 --- a/czech-file-knife/.github/workflows/ghcr-publish.yml +++ /dev/null @@ -1,65 +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/guix-nix-policy.yml b/czech-file-knife/.github/workflows/guix-nix-policy.yml deleted file mode 100644 index ead3b477..00000000 --- a/czech-file-knife/.github/workflows/guix-nix-policy.yml +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Guix/Guix Package Policy -on: [push, pull_request] - -permissions: read-all - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Enforce Guix primary / Guix fallback - run: | - # Check for package manager files - HAS_GUIX=$(find . -name "*.scm" -o -name ".guix-channel" -o -name "guix.scm" 2>/dev/null | head -1) - HAS_NIX=$(find . -name "*.guix" 2>/dev/null | head -1) - - # Block new package-lock.json, yarn.lock, Gemfile.lock, etc. - NEW_LOCKS=$(git diff --name-only --diff-filter=A HEAD~1 2>/dev/null | grep -E 'package-lock\.json|yarn\.lock|Gemfile\.lock|Pipfile\.lock|poetry\.lock|cargo\.lock' || true) - if [ -n "$NEW_LOCKS" ]; then - echo "⚠️ Lock files detected. Prefer Guix manifests for reproducibility." - fi - - # Prefer Guix, fallback to Guix - if [ -n "$HAS_GUIX" ]; then - echo "✅ Guix package management detected (primary)" - elif [ -n "$HAS_NIX" ]; then - echo "✅ Guix package management detected (fallback)" - else - echo "ℹ️ Consider adding guix.scm or flake.guix for reproducible builds" - fi - - echo "✅ Package policy check passed" 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 5b59919d..00000000 --- a/czech-file-knife/.github/workflows/hypatia-scan.yml +++ /dev/null @@ -1,179 +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 "📤 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet..." - - # Clone gitbot-fleet to temp directory - FLEET_DIR="/tmp/gitbot-fleet-$$" - git clone https://github.com/hyperpolymath/gitbot-fleet.git "$FLEET_DIR" - - # Run submission script - bash "$FLEET_DIR/scripts/submit-finding.sh" hypatia-findings.json - - # Cleanup - rm -rf "$FLEET_DIR" - - echo "✅ Finding submission complete" - - - 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'); - const findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8')); - - const critical = findings.filter(f => f.severity === 'critical').length; - const high = findings.filter(f => f.severity === 'high').length; - - let comment = `## 🔍 Hypatia Security Scan\n\n`; - comment += `**Findings:** ${findings.length} issues detected\n\n`; - comment += `| Severity | Count |\n|----------|-------|\n`; - comment += `| 🔴 Critical | ${critical} |\n`; - comment += `| 🟠 High | ${high} |\n`; - comment += `| 🟡 Medium | ${findings.length - critical - high} |\n\n`; - - if (critical > 0) { - comment += `⚠️ **Action Required:** Critical security issues found!\n\n`; - } - - comment += `
View findings\n\n`; - comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; - comment += `
\n\n`; - comment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); 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 228dc438..00000000 --- a/czech-file-knife/.github/workflows/instant-sync.yml +++ /dev/null @@ -1,33 +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 7af3b2f9..00000000 --- a/czech-file-knife/.github/workflows/mirror.yml +++ /dev/null @@ -1,144 +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/npm-bun-blocker.yml b/czech-file-knife/.github/workflows/npm-bun-blocker.yml deleted file mode 100644 index a60fed43..00000000 --- a/czech-file-knife/.github/workflows/npm-bun-blocker.yml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: NPM/Bun Blocker -on: [push, pull_request] - -permissions: read-all - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Block npm/pnpm/yarn lockfiles - run: | - if [ -f "package-lock.json" ] || [ -f "pnpm-lock.yaml" ] || [ -f "yarn.lock" ]; then - echo "❌ npm/pnpm/yarn artifacts detected. Use Bun (package.json + bun.lock) instead." - exit 1 - fi - echo "✅ No npm/pnpm/yarn violations" diff --git a/czech-file-knife/.github/workflows/publish.yml b/czech-file-knife/.github/workflows/publish.yml deleted file mode 100644 index f264ca54..00000000 --- a/czech-file-knife/.github/workflows/publish.yml +++ /dev/null @@ -1,152 +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/quality.yml b/czech-file-knife/.github/workflows/quality.yml deleted file mode 100644 index a6304c4f..00000000 --- a/czech-file-knife/.github/workflows/quality.yml +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Code Quality -on: [push, pull_request] - - -permissions: read-all - -jobs: - lint: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Check file permissions - run: | - find . -type f -perm /111 -name "*.sh" | head -10 || true - - - name: Check for secrets - uses: trufflesecurity/trufflehog@05cccb53bc9e13bc6d17997db5a6bcc3df44bf2f # v3.92.3 - with: - path: ./ - base: ${{ github.event.pull_request.base.sha || github.event.before }} - head: ${{ github.sha }} - continue-on-error: true - - - name: Check TODO/FIXME - run: | - echo "=== TODOs ===" - grep -rn "TODO\|FIXME\|HACK\|XXX" --include="*.rs" --include="*.res" --include="*.py" --include="*.ex" . | head -20 || echo "None found" - - - name: Check for large files - run: | - find . -type f -size +1M -not -path "./.git/*" | head -10 || echo "No large files" - - - name: EditorConfig check - uses: editorconfig-checker/action-editorconfig-checker@4b6cd6190d435e7e084fb35e36a096e98506f7b9 # v2.1.0 - continue-on-error: true - - docs: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Check documentation - run: | - MISSING="" - [ ! -f "README.md" ] && [ ! -f "README.adoc" ] && MISSING="$MISSING README" - [ ! -f "LICENSE" ] && [ ! -f "LICENSE.txt" ] && [ ! -f "LICENSE.md" ] && MISSING="$MISSING LICENSE" - [ ! -f "CONTRIBUTING.md" ] && [ ! -f "../../../.github/CONTRIBUTING.md" ] && MISSING="$MISSING CONTRIBUTING" - - if [ -n "$MISSING" ]; then - echo "::warning::Missing docs:$MISSING" - else - echo "✅ Core documentation present" - fi diff --git a/czech-file-knife/.github/workflows/release.yml b/czech-file-knife/.github/workflows/release.yml deleted file mode 100644 index f7fdb3c1..00000000 --- a/czech-file-knife/.github/workflows/release.yml +++ /dev/null @@ -1,180 +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 f329b096..00000000 --- a/czech-file-knife/.github/workflows/scorecard-enforcer.yml +++ /dev/null @@ -1,57 +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 d50c271a..00000000 --- a/czech-file-knife/.github/workflows/scorecard.yml +++ /dev/null @@ -1,32 +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 b4a74683..00000000 --- a/czech-file-knife/.github/workflows/secret-scanner.yml +++ /dev/null @@ -1,67 +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 - - gitleaks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - - name: Gitleaks Secret Scan - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # 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/security-policy.yml b/czech-file-knife/.github/workflows/security-policy.yml deleted file mode 100644 index 6ca34da5..00000000 --- a/czech-file-knife/.github/workflows/security-policy.yml +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Security Policy -on: [push, pull_request] - -permissions: read-all - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Security checks - run: | - FAILED=false - - # Block MD5/SHA1 for security (allow for checksums/caching) - WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) - if [ -n "$WEAK_CRYPTO" ]; then - echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" - echo "$WEAK_CRYPTO" - fi - - # Block HTTP URLs (except localhost) - HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) - if [ -n "$HTTP_URLS" ]; then - echo "⚠️ HTTP URLs found. Use HTTPS:" - echo "$HTTP_URLS" - fi - - # Block hardcoded secrets patterns - SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) - if [ -n "$SECRETS" ]; then - echo "❌ Potential hardcoded secrets detected!" - FAILED=true - fi - - if [ "$FAILED" = true ]; then - exit 1 - fi - - echo "✅ Security policy check passed" 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 38123875..00000000 --- a/czech-file-knife/.github/workflows/stress-test.yml +++ /dev/null @@ -1,54 +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/.github/workflows/ts-blocker.yml b/czech-file-knife/.github/workflows/ts-blocker.yml deleted file mode 100644 index 1ec0193c..00000000 --- a/czech-file-knife/.github/workflows/ts-blocker.yml +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: TypeScript/JavaScript Blocker -on: [push, pull_request] - -permissions: read-all - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Block new TypeScript/JavaScript - run: | - NEW_TS=$(git diff --name-only --diff-filter=A HEAD~1 2>/dev/null | grep -E '\.(ts|tsx)$' | grep -v '\.gen\.' || true) - NEW_JS=$(git diff --name-only --diff-filter=A HEAD~1 2>/dev/null | grep -E '\.(js|jsx)$' | grep -v '\.res\.js$' | grep -v '\.gen\.' | grep -v 'node_modules' || true) - - if [ -n "$NEW_TS" ] || [ -n "$NEW_JS" ]; then - echo "❌ New TS/JS files detected. Use AffineScript instead." - [ -n "$NEW_TS" ] && echo "$NEW_TS" - [ -n "$NEW_JS" ] && echo "$NEW_JS" - exit 1 - fi - echo "✅ AffineScript policy enforced" diff --git a/czech-file-knife/.github/workflows/wellknown-enforcement.yml b/czech-file-knife/.github/workflows/wellknown-enforcement.yml deleted file mode 100644 index 946c3a3f..00000000 --- a/czech-file-knife/.github/workflows/wellknown-enforcement.yml +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: Well-Known Standards (RFC 9116 + RSR) -on: - push: - branches: [main, master] - paths: - - '.well-known/**' - - 'security.txt' - pull_request: - paths: - - '.well-known/**' - schedule: - # Weekly expiry check - - cron: '0 9 * * *' - workflow_dispatch: - - -permissions: read-all - -jobs: - validate: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: RFC 9116 security.txt validation - run: | - SECTXT="" - [ -f ".well-known/security.txt" ] && SECTXT=".well-known/security.txt" - [ -f "security.txt" ] && SECTXT="security.txt" - - if [ -z "$SECTXT" ]; then - echo "::warning::No security.txt found. See https://github.com/hyperpolymath/well-known-ecosystem" - exit 0 - fi - - # Required: Contact - grep -q "^Contact:" "$SECTXT" || { echo "::error::Missing Contact field"; exit 1; } - - # Required: Expires - if ! grep -q "^Expires:" "$SECTXT"; then - echo "::error::Missing Expires field" - exit 1 - fi - - # Check expiry - EXPIRES=$(grep "^Expires:" "$SECTXT" | cut -d: -f2- | tr -d ' ' | head -1) - if date -d "$EXPIRES" > /dev/null 2>&1; then - DAYS=$(( ($(date -d "$EXPIRES" +%s) - $(date +%s)) / 86400 )) - if [ $DAYS -lt 0 ]; then - echo "::error::security.txt EXPIRED" - exit 1 - elif [ $DAYS -lt 30 ]; then - echo "::warning::security.txt expires in $DAYS days" - else - echo "✅ security.txt valid ($DAYS days)" - fi - fi - - - name: RSR well-known compliance - run: | - MISSING="" - [ ! -f ".well-known/security.txt" ] && [ ! -f "security.txt" ] && MISSING="$MISSING security.txt" - [ ! -f ".well-known/ai.txt" ] && MISSING="$MISSING ai.txt" - [ ! -f ".well-known/humans.txt" ] && MISSING="$MISSING humans.txt" - - if [ -n "$MISSING" ]; then - echo "::warning::Missing RSR recommended files:$MISSING" - echo "Reference: https://github.com/hyperpolymath/well-known-ecosystem/.well-known/" - else - echo "✅ RSR well-known compliant" - fi - - - name: Mixed content check - run: | - MIXED=$(grep -rE 'src="http://|href="http://' --include="*.html" --include="*.htm" . 2>/dev/null | grep -vE 'localhost|127\.0\.0\.1|example\.com' | head -5 || true) - if [ -n "$MIXED" ]; then - echo "::error::Mixed content (HTTP in HTML)" - echo "$MIXED" - exit 1 - fi - echo "✅ No mixed content" - - - name: DNS security records check - if: hashFiles('CNAME') != '' - run: | - DOMAIN=$(cat CNAME 2>/dev/null | tr -d '\n') - if [ -n "$DOMAIN" ]; then - echo "Checking DNS for $DOMAIN..." - # CAA record - dig +short CAA "$DOMAIN" | grep -q "issue" && echo "✅ CAA record" || echo "::warning::No CAA record" - # DNSSEC - dig +dnssec +short "$DOMAIN" | grep -q "RRSIG" && echo "✅ DNSSEC" || echo "::warning::No DNSSEC" - fi diff --git a/czech-file-knife/.github/workflows/workflow-linter.yml b/czech-file-knife/.github/workflows/workflow-linter.yml deleted file mode 100644 index fb8260bf..00000000 --- a/czech-file-knife/.github/workflows/workflow-linter.yml +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# workflow-linter.yml - Validates GitHub workflows against RSR security standards -# This workflow can be copied to other repos for consistent enforcement -name: Workflow Security Linter - -on: - push: - paths: - - '.github/workflows/**' - pull_request: - paths: - - '.github/workflows/**' - workflow_dispatch: - -permissions: read-all - -jobs: - lint-workflows: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Check SPDX Headers - run: | - echo "=== Checking SPDX License Headers ===" - failed=0 - for file in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$file" ] || continue - if ! head -1 "$file" | grep -q "^# SPDX-License-Identifier:"; then - echo "ERROR: $file missing SPDX header" - failed=1 - fi - done - if [ $failed -eq 1 ]; then - echo "Add '# SPDX-License-Identifier: MPL-2.0' as first line" - exit 1 - fi - echo "All workflows have SPDX headers" - - - name: Check Permissions Declaration - run: | - echo "=== Checking Permissions ===" - failed=0 - for file in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$file" ] || continue - if ! grep -q "^permissions:" "$file"; then - echo "ERROR: $file missing top-level 'permissions:' declaration" - failed=1 - fi - done - if [ $failed -eq 1 ]; then - echo "Add 'permissions: read-all' at workflow level" - exit 1 - fi - echo "All workflows have permissions declared" - - - name: Check SHA-Pinned Actions - run: | - echo "=== Checking Action Pinning ===" - # Find actual YAML uses: declarations that aren't SHA-pinned - # Excludes: comments, grep commands, echo statements - unpinned=$(grep -rn "uses:" .github/workflows/ | \ - grep -v "@[a-f0-9]\{40\}" | \ - grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" | \ - grep -v 'grep.*uses:\|echo.*uses:\|#.*uses:' || true) - - if [ -n "$unpinned" ]; then - echo "ERROR: Found unpinned actions:" - echo "$unpinned" - echo "" - echo "Replace version tags with SHA pins, e.g.:" - echo " uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1" - exit 1 - fi - echo "All actions are SHA-pinned" - - - name: Check for Duplicate Workflows - run: | - echo "=== Checking for Duplicates ===" - # Known duplicate patterns - if [ -f .github/workflows/codeql.yml ] && [ -f .github/workflows/codeql-analysis.yml ]; then - echo "ERROR: Duplicate CodeQL workflows found" - echo "Delete codeql-analysis.yml (keep codeql.yml)" - exit 1 - fi - if [ -f .github/workflows/rust.yml ] && [ -f .github/workflows/rust-ci.yml ]; then - echo "WARNING: Potential duplicate Rust workflows" - echo "Consider consolidating rust.yml and rust-ci.yml" - fi - echo "No critical duplicates found" - - - name: Check CodeQL Language Matrix - run: | - echo "=== Checking CodeQL Configuration ===" - if [ ! -f .github/workflows/codeql.yml ]; then - echo "No CodeQL workflow found (optional)" - exit 0 - fi - - # Detect repo languages - has_js=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1) - has_py=$(find . -name "*.py" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1) - has_go=$(find . -name "*.go" -path "*/src/*" -o -path "*/cmd/*" -o -path "*/pkg/*" 2>/dev/null | head -1) - has_rs=$(find . -name "*.rs" -path "*/src/*" 2>/dev/null | head -1) - has_java=$(find . -name "*.java" -path "*/src/*" 2>/dev/null | head -1) - has_rb=$(find . -name "*.rb" -path "*/lib/*" -o -path "*/app/*" 2>/dev/null | head -1) - - echo "Detected languages:" - [ -n "$has_js" ] && echo " - javascript-typescript" - [ -n "$has_py" ] && echo " - python" - [ -n "$has_go" ] && echo " - go" - [ -n "$has_rs" ] && echo " - rust (note: CodeQL rust is limited)" - [ -n "$has_java" ] && echo " - java-kotlin" - [ -n "$has_rb" ] && echo " - ruby" - - # Check for over-reach - if grep -q "language:.*'go'" .github/workflows/codeql.yml && [ -z "$has_go" ]; then - echo "WARNING: CodeQL configured for Go but no Go files found" - fi - if grep -q "language:.*'python'" .github/workflows/codeql.yml && [ -z "$has_py" ]; then - echo "WARNING: CodeQL configured for Python but no Python files found" - fi - if grep -q "language:.*'java'" .github/workflows/codeql.yml && [ -z "$has_java" ]; then - echo "WARNING: CodeQL configured for Java but no Java files found" - fi - if grep -q "language:.*'ruby'" .github/workflows/codeql.yml && [ -z "$has_rb" ]; then - echo "WARNING: CodeQL configured for Ruby but no Ruby files found" - fi - - echo "CodeQL check complete" - - - name: Check Secrets Guards - run: | - echo "=== Checking Secrets Usage ===" - # Look for secrets without conditional guards in mirror workflows - if [ -f .github/workflows/mirror.yml ]; then - if grep -q "secrets\." .github/workflows/mirror.yml; then - if ! grep -q "if:.*vars\." .github/workflows/mirror.yml; then - echo "WARNING: mirror.yml uses secrets without vars guard" - echo "Add 'if: vars.FEATURE_ENABLED == true' to jobs" - fi - fi - fi - echo "Secrets check complete" - - - name: Summary - run: | - echo "" - echo "=== Workflow Linter Summary ===" - echo "All critical checks passed." - echo "" - echo "For more info, see: robot-repo-bot/ERROR-CATALOG.scm" diff --git a/czech-file-knife/.gitignore b/czech-file-knife/.gitignore deleted file mode 100644 index 73f3573f..00000000 --- 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 44ebc6d5..00000000 --- a/czech-file-knife/.gitlab-ci.yml +++ /dev/null @@ -1,173 +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 \ No newline at end of file diff --git a/czech-file-knife/.machine_readable/6a2/AGENTIC.a2ml b/czech-file-knife/.machine_readable/6a2/AGENTIC.a2ml deleted file mode 100644 index 28bf1d9c..00000000 --- a/czech-file-knife/.machine_readable/6a2/AGENTIC.a2ml +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# AGENTIC.a2ml — AI agent constraints and capabilities -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[agent-permissions] -can-edit-source = true -can-edit-tests = true -can-edit-docs = true -can-edit-config = true -can-create-files = true - -[agent-constraints] -# What AI agents must NOT do: -# - Never use banned language patterns (believe_me, unsafeCoerce, etc.) -# - Never commit secrets or credentials -# - Never use banned languages (TypeScript, Python, Go, etc.) -# - Never place state files in repository root (must be in .machine_readable/) -# - Never relicense an existing file, and never run an automated licence -# sweep (LICENCE-POLICY.adoc A2). New files get correct SPDX from birth. -# - Never assume a licence. Read standards/LICENCE-POLICY.adoc: Rule 1 -# defaults to MPL-2.0 (code) / CC-BY-SA-4.0 (prose), but Rule 3 -# (co-developed), Rule 4 (network-deployed services) and Rule 5 -# (games) are AGPL-3.0-or-later, and Rule 2 names the PMPL register. - -[maintenance-integrity] -fail-closed = true -require-evidence-per-step = true -allow-silent-skip = false -require-rerun-after-fix = true -release-claim-requires-hard-pass = true - -[automation-hooks] -# on-enter: Read 0-AI-MANIFEST.a2ml, then STATE.a2ml -# on-exit: Update STATE.a2ml with session outcomes -# on-commit: Run just validate-rsr diff --git a/czech-file-knife/.machine_readable/6a2/ECOSYSTEM.a2ml b/czech-file-knife/.machine_readable/6a2/ECOSYSTEM.a2ml deleted file mode 100644 index b8899075..00000000 --- a/czech-file-knife/.machine_readable/6a2/ECOSYSTEM.a2ml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# ECOSYSTEM.a2ml — Czech File Knife ecosystem position -[metadata] -version = "1.0" -last-updated = "2026-04-11" - -[project] -name = "Czech File Knife" -purpose = "" -role = "" - -[position-in-ecosystem] -category = "" - -[related-projects] -projects = [ - # No related projects recorded -] diff --git a/czech-file-knife/.machine_readable/6a2/META.a2ml b/czech-file-knife/.machine_readable/6a2/META.a2ml deleted file mode 100644 index 914ffc31..00000000 --- a/czech-file-knife/.machine_readable/6a2/META.a2ml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# META.a2ml — Czech File Knife meta-level information -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[project-info] -license = "MPL-2.0" -author = "Jonathan D.A. Jewell (hyperpolymath)" - -[architecture-decisions] -decisions = [ - # No ADRs recorded -] - -[development-practices] -versioning = "SemVer" -documentation = "AsciiDoc" -build-tool = "just" - -[maintenance-axes] -scoping-first = true -axis-1 = "must > intend > like" -axis-2 = "corrective > adaptive > perfective" -axis-3 = "systems > compliance > effects" diff --git a/czech-file-knife/.machine_readable/6a2/NEUROSYM.a2ml b/czech-file-knife/.machine_readable/6a2/NEUROSYM.a2ml deleted file mode 100644 index e1d34c09..00000000 --- a/czech-file-knife/.machine_readable/6a2/NEUROSYM.a2ml +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# NEUROSYM.a2ml — Neurosymbolic integration metadata -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[hypatia-config] -scan-enabled = true -scan-depth = "standard" # quick | standard | deep -report-format = "logtalk" - -[symbolic-rules] -# Custom symbolic rules for this project -# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" } - -[neural-config] -# Neural pattern detection settings -# confidence-threshold = 0.85 -# model = "hypatia-v2" diff --git a/czech-file-knife/.machine_readable/6a2/PLAYBOOK.a2ml b/czech-file-knife/.machine_readable/6a2/PLAYBOOK.a2ml deleted file mode 100644 index 5003fd08..00000000 --- a/czech-file-knife/.machine_readable/6a2/PLAYBOOK.a2ml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# PLAYBOOK.a2ml — Operational playbook -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[deployment] -# method = "gitops" # gitops | manual | ci-triggered -# target = "container" # container | binary | library | wasm - -[incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status -# 2. Review recent commits and CI results -# 3. Run `just validate` to check compliance -# 4. Run `just security` to audit for vulnerabilities - -[release-process] -# 1. Update version in STATE.a2ml, META.a2ml -# 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) -# 3. Tag and push - -[maintenance-operations] -# Baseline audit: just maint-audit -# Hard release gate: just maint-hard-pass diff --git a/czech-file-knife/.machine_readable/6a2/STATE.a2ml b/czech-file-knife/.machine_readable/6a2/STATE.a2ml deleted file mode 100644 index cecc4504..00000000 --- a/czech-file-knife/.machine_readable/6a2/STATE.a2ml +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# STATE.a2ml — Czech File Knife project state -[metadata] -project = "czech-file-knife" -version = "0.0.1" -last-updated = "2026-02-01" -status = "active" -session = "converted from scheme — 2026-04-11" - -[project-context] -name = "Czech File Knife" -purpose = """""" -completion-percentage = 0 - -[position] -phase = "initial" # design | implementation | testing | maintenance | archived -maturity = "experimental" # experimental | alpha | beta | production | lts - -[route-to-mvp] -milestones = [ - # No milestones recorded -] - -[blockers-and-issues] -issues = [ - # No blockers recorded -] - -[critical-next-actions] -actions = [ - # No actions recorded -] - -[maintenance-status] -last-run-utc = "2026-02-01T00:00:00Z" -last-result = "unknown" # unknown | pass | warn | fail diff --git a/czech-file-knife/.nojekyll b/czech-file-knife/.nojekyll deleted file mode 100644 index e69de29b..00000000 diff --git a/czech-file-knife/.tool-versions b/czech-file-knife/.tool-versions deleted file mode 100644 index 4564042e..00000000 --- a/czech-file-knife/.tool-versions +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# asdf/mise tool versions -# See: https://asdf-vm.com/ - -rust nightly diff --git a/czech-file-knife/ABI-FFI-README.adoc b/czech-file-knife/ABI-FFI-README.adoc deleted file mode 100644 index 6d7714d5..00000000 --- a/czech-file-knife/ABI-FFI-README.adoc +++ /dev/null @@ -1,407 +0,0 @@ -== CZECH_FILE_KNIFE 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/czech-file-knife.h │ -└─────────────────┬───────────────────────────┘ - │ - │ imported by - ▼ -┌─────────────────────────────────────────────┐ -│ FFI Implementation (Zig) │ -│ ffi/zig/src/main.zig │ -│ - Implements C-compatible functions │ -│ - Zero-cost abstractions │ -│ - Memory-safe by default │ -└─────────────────┬───────────────────────────┘ - │ - │ compiled to libczech-file-knife.so/.a - ▼ -┌─────────────────────────────────────────────┐ -│ Any Language via C ABI │ -│ - Rust, AffineScript, Julia, Python, etc. │ -└─────────────────────────────────────────────┘ -.... - -=== Directory Structure - -.... -czech-file-knife/ -├── 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/ -│ └── czech-file-knife.h # C header (optional, can be generated) -│ -├── generated/ # Auto-generated files -│ └── abi/ -│ └── czech-file-knife.h # Generated from Idris2 ABI -│ -└── bindings/ # Language-specific wrappers (optional) - ├── rust/ - ├── affinescript/ - └── 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/czech-file-knife.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 "czech-file-knife.h" - -int main() { - void* handle = czech-file-knife_init(); - if (!handle) return 1; - - int result = czech-file-knife_process(handle, 42); - if (result != 0) { - const char* err = czech-file-knife_last_error(); - fprintf(stderr, "Error: %s\n", err); - } - - czech-file-knife_free(handle); - return 0; -} ----- - -Compile with: - -[source,bash] ----- -gcc -o example example.c -lczech-file-knife -L./zig-out/lib ----- - -==== From Idris2 - -[source,idris] ----- -import CZECH_FILE_KNIFE.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 = "czech-file-knife")] -extern "C" { - fn czech-file-knife_init() -> *mut std::ffi::c_void; - fn czech-file-knife_free(handle: *mut std::ffi::c_void); - fn czech-file-knife_process(handle: *mut std::ffi::c_void, input: u32) -> i32; -} - -fn main() { - unsafe { - let handle = czech-file-knife_init(); - assert!(!handle.is_null()); - - let result = czech-file-knife_process(handle, 42); - assert_eq!(result, 0); - - czech-file-knife_free(handle); - } -} ----- - -==== From Julia - -[source,julia] ----- -const libczech-file-knife = "libczech-file-knife" - -function init() - handle = ccall((:czech-file-knife_init, libczech-file-knife), Ptr{Cvoid}, ()) - handle == C_NULL && error("Failed to initialize") - handle -end - -function process(handle, input) - result = ccall((:czech-file-knife_process, libczech-file-knife), Cint, (Ptr{Cvoid}, UInt32), handle, input) - result -end - -function cleanup(handle) - ccall((:czech-file-knife_free, libczech-file-knife), 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/czech-file-knife.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 - -MPL-2.0 - -=== 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/CODE_OF_CONDUCT.adoc b/czech-file-knife/CODE_OF_CONDUCT.adoc deleted file mode 100644 index a6003933..00000000 --- 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 61a4f760..00000000 --- a/czech-file-knife/CONTRIBUTING.adoc +++ /dev/null @@ -1,108 +0,0 @@ -== Clone the repository - -git clone https://github.com/hyperpolymath/ambientops.git cd ambientops - -== Using Guix (recommended for reproducibility) - -guix develop - -== Or using toolbox/distrobox - -toolbox create ambientops-dev toolbox enter ambientops-dev # Install -dependencies manually - -== Verify setup - -just check # or: cargo check / mix compile / etc. just test # Run test -suite - -.... - -### Repository Structure -.... - -ambientops/ ├── 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.guix # Guix -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` -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://github.com/hyperpolymath/ambientops/labels/good%20first%20issue) — Simple Perimeter 3 tasks -- [`help wanted`](https://github.com/hyperpolymath/ambientops/labels/help%20wanted) — Community help needed -- [`documentation`](https://github.com/hyperpolymath/ambientops/labels/documentation) — Docs improvements -- [`perimeter-3`](https://github.com/hyperpolymath/ambientops/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 cc754beb..00000000 --- a/czech-file-knife/Cargo.lock +++ /dev/null @@ -1,7120 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[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.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a46f56d354df11b6bcd8ca4f84fa03ac816cc41c72568a1b337d8f7e5af90e" -dependencies = [ - "arc-swap", - "async-task", - "crossbeam-deque", - "crossbeam-utils", - "libc", - "num_cpus", - "parking_lot 0.12.5", - "thiserror 2.0.21", - "winapi", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "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.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850babe4998b8b9e2dad65a56be4ee1bb977bc3cb3934fa2b28feb2297c30a55" -dependencies = [ - "cssparser", - "html5ever", - "maplit", - "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 = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -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 = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - -[[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 = "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 = "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-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[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.117", -] - -[[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.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[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.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "sync_wrapper", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bcrypt" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a0f5948f30df5f43ac29d310b7476793be97c50787e6ef4a63d960a0d0be827" -dependencies = [ - "base64 0.22.1", - "blowfish", - "getrandom 0.3.4", - "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 = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "serde", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitpacking" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" -dependencies = [ - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures 0.3.0", -] - -[[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 = "bnum" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" - -[[package]] -name = "bon" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" -dependencies = [ - "darling", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "boxcar" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[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.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "bytesize" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" - -[[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.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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", - "redb", - "redis", - "serde", - "serde_json", - "sled", - "surrealdb", - "thiserror 2.0.21", - "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.21", - "tokio", -] - -[[package]] -name = "cfk-integrations" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "regex", - "serde", - "serde_json", - "thiserror 2.0.21", - "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.21", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "cfk-providers" -version = "0.1.0" -dependencies = [ - "async-trait", - "blake3", - "bytes", - "cfk-core", - "chrono", - "futures", - "libc", - "native-tls", - "oauth2", - "reqwest 0.13.5", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.21", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "cfk-search" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "chrono", - "regex", - "serde", - "tantivy", - "thiserror 2.0.21", - "tokio", -] - -[[package]] -name = "cfk-vfs" -version = "0.1.0" -dependencies = [ - "async-trait", - "cfk-core", - "cfk-providers", - "dashmap", - "fuser", - "parking_lot 0.12.5", - "thiserror 2.0.21", - "tokio", - "tracing", -] - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[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.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" -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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width", - "windows-sys 0.61.2", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -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-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "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-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[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.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59" -dependencies = [ - "dtoa-short", - "itoa", - "smallvec", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[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.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -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.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "datasketches" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -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", - "const-oid", - "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-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "diskann" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ead115d8a4f3917c1ae9c3db0aa4dd56251841864ef8637329969f8e5145dd3" -dependencies = [ - "anyhow", - "bytemuck", - "diskann-utils", - "diskann-vector", - "diskann-wide", - "futures-util", - "half", - "hashbrown 0.16.1", - "num-traits", - "rand 0.9.4", - "thiserror 2.0.21", - "tokio", -] - -[[package]] -name = "diskann-utils" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac627181d402b5dd63318e434fc2e7107843be0a10cf0a6a99a24666453239bc" -dependencies = [ - "bytemuck", - "cfg-if", - "diskann-vector", - "diskann-wide", - "half", - "rand 0.9.4", - "rand_distr", - "rayon", - "thiserror 2.0.21", -] - -[[package]] -name = "diskann-vector" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee9ba401eba2a12cea01aba7ee2ba7c755019f2b73c2417fecc49a2da26dd4c" -dependencies = [ - "cfg-if", - "diskann-wide", - "half", -] - -[[package]] -name = "diskann-wide" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343f4ea154bad7f8a9c6936746379faa880899165740a20092775bec97c9a141" -dependencies = [ - "cfg-if", - "half", -] - -[[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.117", -] - -[[package]] -name = "dmp" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2dfc7a18dffd3ef60a442b72a827126f1557d914620f8fc4d1049916da43c1" -dependencies = [ - "trice", - "urlencoding", -] - -[[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 0.11.3", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[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 = "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 = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array 0.14.7", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[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 = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[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 = "fastnum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4089ab2dfd45d8ddc92febb5ca80644389d5ebb954f40231274a3f18341762e2" -dependencies = [ - "bnum", - "num-integer", - "num-traits", -] - -[[package]] -name = "fastrand" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" -dependencies = [ - "bitflags 2.11.0", - "rustc_version", - "serde", -] - -[[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 = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[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.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a5eca878900c2e39e9e52fd797954b7fc39eeefc8558257114bfea6a698fcf" -dependencies = [ - "bitflags 2.11.0", - "libc", - "log", - "memchr", - "nix", - "num_enum", - "page_size", - "parking_lot 0.12.5", - "pkg-config", - "ref-cast", - "smallvec", - "zerocopy", -] - -[[package]] -name = "futures" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "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", - "zeroize", -] - -[[package]] -name = "geo" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3901269ec6d4f6068d3f09e5f02f995bd076398dcd1dfec407cd230b02d11b" -dependencies = [ - "earcutr", - "float_next_after", - "geo-types", - "geographiclib-rs", - "i_overlay", - "log", - "num-traits", - "rand 0.8.6", - "robust", - "rstar 0.12.2", - "serde", - "sif-itree", - "spade", -] - -[[package]] -name = "geo-types" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94776032c45f950d30a13af6113c2ad5625316c9abfbccee4dd5a6695f8fe0f5" -dependencies = [ - "approx", - "num-traits", - "rayon", - "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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7" -dependencies = [ - "libm", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", - "wasm-bindgen", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "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 = [ - "bytemuck", - "cfg-if", - "crunchy", - "num-traits", - "rand 0.9.4", - "rand_distr", - "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" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64 0.22.1", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http", -] - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" -dependencies = [ - "bitflags 2.11.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 1.3.3", - "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 = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[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.40.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130" -dependencies = [ - "log", - "markup5ever", - "memchr", -] - -[[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 = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "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.6", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "i_float" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b" -dependencies = [ - "libm", -] - -[[package]] -name = "i_key_sort" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27" - -[[package]] -name = "i_overlay" -version = "4.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413183068e6e0289e18d7d0a1f661b81546e6918d5453a44570b9ab30cbed1b3" -dependencies = [ - "i_float", - "i_key_sort", - "i_shape", - "i_tree", - "rayon", -] - -[[package]] -name = "i_shape" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082" -dependencies = [ - "i_float", -] - -[[package]] -name = "i_tree" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -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.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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 = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.18.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" -dependencies = [ - "console", - "portable-atomic", - "unicode-width", - "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 = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -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.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.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[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 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[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.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonwebtoken" -version = "10.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" -dependencies = [ - "aws-lc-rs", - "base64 0.22.1", - "ed25519-dalek", - "getrandom 0.2.17", - "hmac", - "js-sys", - "p256", - "p384", - "pem", - "rand 0.8.6", - "rsa", - "serde", - "serde_json", - "sha2", - "signature", - "simple_asn1", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[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.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8f89da8fd95c4eb6274e914694bea90c7826523b26f2a2fd863d44b9d42c43" -dependencies = [ - "deunicode", -] - -[[package]] -name = "libc" -version = "0.2.190" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" -dependencies = [ - "libc", -] - -[[package]] -name = "libz-sys" -version = "1.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lmdb-master-sys" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" -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.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4" -version = "1.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" -dependencies = [ - "lz4-sys", -] - -[[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.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" -dependencies = [ - "twox-hash", -] - -[[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.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[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.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[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.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "murmurhash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "ndarray" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - -[[package]] -name = "ndarray-stats" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6e54a8b65764f71827a90ca1d56965ec0c67f069f996477bd493402a901d1f" -dependencies = [ - "indexmap", - "itertools 0.13.0", - "ndarray", - "noisy_float", - "num-integer", - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "noisy_float" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16843be85dd410c6a12251c4eca0dd1d3ee8c5725f746c4d5e0fdcec0a864b2" -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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -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-bigint" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.6", - "smallvec", - "zeroize", -] - -[[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-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "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 = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "oauth2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" -dependencies = [ - "base64 0.22.1", - "chrono", - "getrandom 0.2.17", - "http", - "rand 0.8.6", - "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 = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[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_store" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622acbc9100d3c10e2ee15804b0caa40e55c933d5aa53814cd520805b7958a49" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "futures-channel", - "futures-core", - "futures-util", - "http", - "humantime", - "itertools 0.14.0", - "parking_lot 0.12.5", - "percent-encoding", - "thiserror 2.0.21", - "tokio", - "tracing", - "url", - "walkdir", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[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" -version = "0.10.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - -[[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 = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "papaya" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" -dependencies = [ - "equivalent", - "seize", -] - -[[package]] -name = "papergrid" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" -dependencies = [ - "bytecount", - "fnv", - "unicode-width", -] - -[[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 0.2.1", -] - -[[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 = "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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" -dependencies = [ - "phf_shared 0.14.0", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" -dependencies = [ - "phf_generator 0.14.0", - "phf_shared 0.14.0", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" -dependencies = [ - "fastrand", - "phf_shared 0.14.0", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn 2.0.117", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -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.117", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-types" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" -dependencies = [ - "prost", -] - -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - -[[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.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3" -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.21", - "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.2", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.21", - "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.59.0", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -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 = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "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.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -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.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" -dependencies = [ - "num-traits", - "rand 0.9.4", -] - -[[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.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -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 = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb338a6c67830a61bed824b78c2bb034ab1ff401a20616bd7957524f4fdc2f22" -dependencies = [ - "libc", -] - -[[package]] -name = "redis" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5b98ae99461e02895c3a96a25dbe4d852a3d5e5b815d31d5d9e4b5344d99e9" -dependencies = [ - "arcstr", - "async-lock", - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint 0.5.1", - "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.11.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.21", -] - -[[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.117", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[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", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.6", -] - -[[package]] -name = "reqwest" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" -dependencies = [ - "base64 0.23.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "mime_guess", - "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.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b48b66c1cd4bf814516ea48f727d4b3697e3fa8841be99a7d9cbb8c9ac1666" -dependencies = [ - "bytes", - "chrono", - "geo", - "regex", - "revision-derive", - "roaring", - "rust_decimal", - "uuid", -] - -[[package]] -name = "revision-derive" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d266d798eaaa2921e14188dfe998bb7cc0528ec26e894b4be0e35fe2b19c89d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.17", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "roaring" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" -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 = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[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.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -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.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -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 0.9.0", -] - -[[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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array 0.14.7", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "seize" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[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 = "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 = "sif-itree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f45b8998ced5134fb1d75732c77842a3e888f19c1ff98481822e8fbfbf930b" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "thiserror 2.0.21", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "sketches-ddsketch" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e40b6cf54d988dc1a2223531b969c9a9e30906ad90ef64890c27b4bfbb46ea" -dependencies = [ - "serde", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[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" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spade" -version = "2.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" -dependencies = [ - "hashbrown 0.16.1", - "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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "storekey" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9a94571bde7369ecaac47cec2e6844642d99166bd452fbd8def74b5b917b2f" -dependencies = [ - "bytes", - "storekey-derive", - "uuid", -] - -[[package]] -name = "storekey-derive" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6079d53242246522ec982de613c5c952cc7b1380ef2f8622fcdab9bfe73c0098" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "string_cache" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6" -dependencies = [ - "new_debug_unreachable", - "parking_lot 0.12.5", - "phf_shared 0.14.0", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc" -dependencies = [ - "phf_generator 0.14.0", - "phf_shared 0.14.0", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "surrealdb" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0168afb4502580b010294f6de07d4550622051a54d21ee6e1c418887ab8fe31" -dependencies = [ - "anyhow", - "async-channel", - "boxcar", - "chrono", - "futures", - "getrandom 0.3.4", - "indexmap", - "js-sys", - "path-clean", - "reqwest 0.13.5", - "ring", - "rustls", - "rustls-pki-types", - "semver", - "serde", - "serde_json", - "surrealdb-core", - "surrealdb-types", - "surrealdb-types-derive", - "tokio", - "tokio-tungstenite", - "tokio-tungstenite-wasm", - "tokio-util", - "tracing", - "url", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasmtimer", - "web-sys", -] - -[[package]] -name = "surrealdb-collections" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ec3f806e9e9e7b897d853c0c9a5c518016a81a59dbf6f2c201deac02a61a5c" -dependencies = [ - "revision", - "storekey", -] - -[[package]] -name = "surrealdb-core" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d748dfafac2234fb2c9b81e321a61e8f8baa489b925d64e5c3135ecdf4741c1" -dependencies = [ - "addr", - "affinitypool", - "ahash 0.8.12", - "ammonia", - "anyhow", - "argon2", - "async-channel", - "async-stream", - "base64 0.22.1", - "bcrypt", - "blake3", - "bytes", - "chrono", - "ciborium", - "dashmap", - "deunicode", - "diskann", - "diskann-utils", - "diskann-vector", - "dmp", - "ext-sort", - "fastnum", - "fst", - "futures", - "fuzzy-matcher", - "geo", - "geo-types", - "getrandom 0.3.4", - "half", - "headers", - "hex", - "http", - "humantime", - "ipnet", - "jsonwebtoken", - "lexicmp", - "md-5", - "memchr", - "mime", - "ndarray", - "ndarray-stats", - "num-traits", - "num_cpus", - "object_store", - "parking_lot 0.12.5", - "path-clean", - "pbkdf2", - "phf 0.13.1", - "pin-project-lite", - "quick_cache", - "rand 0.9.4", - "rand_core 0.6.4", - "rayon", - "reblessive", - "regex", - "revision", - "ring", - "roaring", - "rust-stemmers", - "rust_decimal", - "scrypt", - "semver", - "serde", - "serde_json", - "sha1", - "sha2", - "storekey", - "strsim", - "subtle", - "surrealdb-collections", - "surrealdb-protocol", - "surrealdb-rocksdb", - "surrealdb-strand", - "surrealdb-types", - "surrealmx", - "sysinfo", - "tempfile", - "thiserror 2.0.21", - "tokio", - "tokio-util", - "tracing", - "ulid", - "unicase", - "url", - "uuid", - "vart", - "wasm-bindgen-futures", - "wasmtimer", - "web-time", -] - -[[package]] -name = "surrealdb-librocksdb-sys" -version = "0.18.3+11.0.0-4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b82a18c7fa4b57206784a1a31e7b942ae1d3e24493e0c733019a409b2b4bea" -dependencies = [ - "bindgen", - "bzip2-sys", - "cc", - "libc", - "libz-sys", - "lz4-sys", - "zstd-sys", -] - -[[package]] -name = "surrealdb-protocol" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4e06f586c9179a02349b88b0c18e3a0850c55431aa513e0cd66529c00da1af" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "chrono", - "flatbuffers", - "futures", - "geo", - "prost", - "prost-types", - "rust_decimal", - "semver", - "serde", - "serde_json", - "tonic", - "tonic-prost", - "uuid", -] - -[[package]] -name = "surrealdb-rocksdb" -version = "0.24.0-surreal.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e8c3a004982458af159bcbf369e41663d538cd4a291a49c0d4a2fb373cbb7e" -dependencies = [ - "libc", - "surrealdb-librocksdb-sys", -] - -[[package]] -name = "surrealdb-strand" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95c31425eff6beaca846bc5fd0961966d6b71fa9e9dd478275f22792957d33b" -dependencies = [ - "revision", - "serde", - "storekey", -] - -[[package]] -name = "surrealdb-types" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f8d4f15c5cd086ff11385a1b482c0e00786479d423be3cb32f29ce5d7e9165" -dependencies = [ - "anyhow", - "async-channel", - "bytes", - "castaway", - "chrono", - "flatbuffers", - "geo", - "hex", - "http", - "papaya", - "rand 0.9.4", - "regex", - "rust_decimal", - "semver", - "serde", - "serde_json", - "surrealdb-protocol", - "surrealdb-types-derive", - "tracing", - "ulid", - "url", - "uuid", -] - -[[package]] -name = "surrealdb-types-derive" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c14b08a1c4be42d5769a7501da5ec9bb313f5e3ff566696d4c7cb1234f61d61" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "surrealmx" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8a87f050a4860832ccf4a53e43ea264e98d27618983602b5c575e6df296054" -dependencies = [ - "arc-swap", - "bincode 2.0.1", - "bytes", - "crossbeam-deque", - "crossbeam-queue", - "crossbeam-skiplist", - "lz4", - "papaya", - "parking_lot 0.12.5", - "serde", - "smallvec", - "thiserror 2.0.21", - "tracing", - "web-time", -] - -[[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.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -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.117", -] - -[[package]] -name = "sysinfo" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tabled" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" -dependencies = [ - "papergrid", - "tabled_derive", - "testing_table", -] - -[[package]] -name = "tabled_derive" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" -dependencies = [ - "heck 0.5.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tantivy" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "861facfabd71044968f364837f9a083b56464ba5a59079f88706ee5c451ca069" -dependencies = [ - "aho-corasick", - "arc-swap", - "base64 0.22.1", - "bitpacking", - "bon", - "byteorder", - "census", - "crc32fast", - "crossbeam-channel", - "datasketches", - "downcast-rs", - "fastdivide", - "fnv", - "fs4", - "htmlescape", - "itertools 0.14.0", - "levenshtein_automata", - "log", - "lru", - "lz4_flex", - "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.21", - "time", - "typetag", - "uuid", - "winapi", -] - -[[package]] -name = "tantivy-bitpacker" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4" -dependencies = [ - "bitpacking", -] - -[[package]] -name = "tantivy-columnar" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" -dependencies = [ - "downcast-rs", - "fastdivide", - "itertools 0.14.0", - "serde", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-sstable", - "tantivy-stacker", -] - -[[package]] -name = "tantivy-common" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5" -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.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" -dependencies = [ - "fnv", - "nom", - "ordered-float", - "serde", - "serde_json", -] - -[[package]] -name = "tantivy-sstable" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606" -dependencies = [ - "futures-util", - "itertools 0.14.0", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-fst", - "zstd", -] - -[[package]] -name = "tantivy-stacker" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951" -dependencies = [ - "murmurhash32", - "tantivy-common", -] - -[[package]] -name = "tantivy-tokenizer-api" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98" -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.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "testing_table" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" -dependencies = [ - "unicode-width", -] - -[[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.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl 2.0.21", -] - -[[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.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[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 = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -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.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-tungstenite-wasm" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee909c02b8863f9bda87253127eb4da0e7e1342330b2583fbc4d1795c2f8" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "httparse", - "js-sys", - "thiserror 2.0.21", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.10+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "axum", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "indexmap", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.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.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -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.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -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.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.4", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 2.0.21", - "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 = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "typetag" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" -dependencies = [ - "erased-serde", - "inventory", - "once_cell", - "serde", - "typetag-impl", -] - -[[package]] -name = "typetag-impl" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand 0.9.4", - "serde", - "web-time", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[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.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[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.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" -dependencies = [ - "getrandom 0.4.2", - "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.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 = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - -[[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.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "serde", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.12.5", - "pin-utils", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -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.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada" -dependencies = [ - "phf 0.14.0", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -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.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -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.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[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.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[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", - "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-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[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_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_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_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[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_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_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_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 = "winnow" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[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.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[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 9e9ca2d5..00000000 --- a/czech-file-knife/Cargo.toml +++ /dev/null @@ -1,100 +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 = "MPL-2.0" -repository = "https://github.com/hyperpolymath/czech-file-knife" -homepage = "https://github.com/hyperpolymath/czech-file-knife" -documentation = "https://docs.rs/cfk-core" -authors = ["Jonathan D.A. Jewell "] -keywords = ["file-manager", "cloud-storage", "fuse", "sync"] -categories = ["command-line-utilities", "filesystem"] - -[workspace.dependencies] -# Async runtime -tokio = { version = "1.52", 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.8" -lz4_flex = "0.13" -hex = "0.4" - -# Time & Errors -chrono = { version = "0.4", features = ["serde"] } -thiserror = "2.0" - -# CLI -clap = { version = "4.6", features = ["derive"] } -indicatif = "0.18" -console = "0.16" -dialoguer = "0.11" -tabled = "0.20" -bytesize = "2.3" - -# Storage -sled = "0.34" -directories = "6.0" - -# Search -fuzzy-matcher = "0.3" -tantivy = "0.26" -glob = "0.3" -regex = "1.12" -walkdir = "2.4" -ignore = "0.4" -infer = "0.15" - -# FUSE -fuser = "0.17" -parking_lot = "0.12" -dashmap = "6.2" - -# Bytes -bytes = "1.11" -lru = "0.16.4" - -# FFI -libc = "0.2" - -once_cell = "1.19" - -# URL parsing -url = "2.5" - -# TLS -native-tls = "0.2" - -# 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 9cc45e61..00000000 --- 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.85-slim-bookworm 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="MPL-2.0" - -# 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 007a1d03..00000000 --- a/czech-file-knife/Justfile +++ /dev/null @@ -1,29 +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" diff --git a/czech-file-knife/LICENSE b/czech-file-knife/LICENSE deleted file mode 100644 index ec540b34..00000000 --- a/czech-file-knife/LICENSE +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2024-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 LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/czech-file-knife/MAINTAINERS.adoc b/czech-file-knife/MAINTAINERS.adoc deleted file mode 100644 index 48d97817..00000000 --- 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 5f075413..00000000 --- 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 b2d5c0bc..00000000 --- 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 **MPL-2.0**. - -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 a689e5bc..00000000 --- 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 - -MPL-2.0 OR Palimpsest-0.8 diff --git a/czech-file-knife/ROADMAP.adoc b/czech-file-knife/ROADMAP.adoc deleted file mode 100644 index 10beba74..00000000 --- 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 2215481b..00000000 --- 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, AffineScript -* **Tier 2** (Silver): Nickel, Racket, Guile Scheme, Guix -* **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.guix` - -=== Prohibited - -* Python outside `salt/` directory -* TypeScript/JavaScript (use AffineScript) -* 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 abb6fbb5..00000000 --- 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 3a8e4e90..00000000 --- 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 13782276..00000000 --- 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 566e1ce7..00000000 --- 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 88d6bf71..00000000 --- a/czech-file-knife/cfk-cache/Cargo.toml +++ /dev/null @@ -1,38 +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 = "3.1", optional = true, features = ["kv-mem", "kv-rocksdb"] } - - -redb = { version = "4.1", optional = true } -heed = { version = "0.22", optional = true } # LMDB wrapper -redis = { version = "1.2", 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 97ab2257..00000000 --- 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 779ba128..00000000 --- a/czech-file-knife/cfk-cache/src/lib.rs +++ /dev/null @@ -1,178 +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 - -#![forbid(unsafe_code)] -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 00432bb7..00000000 --- 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 dc9ba9e0..00000000 --- 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 8f32bb9e..00000000 --- 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 b9c3b1e7..00000000 --- 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 fd5cb389..00000000 --- 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 d46f16a7..00000000 --- a/czech-file-knife/cfk-cli/src/main.rs +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Czech File Knife CLI -//! -//! A cloud-native, universal file management tool. - -#![forbid(unsafe_code)] -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 a5186be5..00000000 --- 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 93007d09..00000000 --- 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 7f629265..00000000 --- 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 da1c3df7..00000000 --- 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 04f263fb..00000000 --- a/czech-file-knife/cfk-core/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Czech File Knife Core -//! -//! Core traits, types, and abstractions for the unified filesystem interface. - -#![forbid(unsafe_code)] -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 b260fb7b..00000000 --- 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 2682c00d..00000000 --- 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 9b680fea..00000000 --- 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 e47c587f..00000000 --- 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 893f2fa5..00000000 --- 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 afb8aef8..00000000 --- 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 d836ce62..00000000 --- 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 d6f651c9..00000000 --- a/czech-file-knife/cfk-integrations/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! External tool integrations for Czech File Knife -//! -//! Integrates: aria2, agrep, pandoc, tesseract OCR, eza - -#![forbid(unsafe_code)] -#[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 fd840a00..00000000 --- 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 05c5e483..00000000 --- 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 d79d1328..00000000 --- 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 ee973b7a..00000000 --- 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 0c3a42b1..00000000 --- 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 d7343fef..00000000 --- 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 80f1758d..00000000 --- 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 d25bd78f..00000000 --- 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 b1211ccc..00000000 --- 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 4d3a0aeb..00000000 --- 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 43c4721b..00000000 --- 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 f1580e34..00000000 --- a/czech-file-knife/cfk-providers/Cargo.toml +++ /dev/null @@ -1,45 +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 -url.workspace = true -native-tls.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 c94eae4d..00000000 --- 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 d89eb54f..00000000 --- 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 1e259c43..00000000 --- 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 dfd2ab03..00000000 --- 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 2d872f5e..00000000 --- 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 bb527a17..00000000 --- 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 7cb25424..00000000 --- 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 94165249..00000000 --- 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 c71aaa8b..00000000 --- a/czech-file-knife/cfk-providers/src/nfs.rs +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! NFS storage backend -//! -//! Network File System client implementation. -//! Supports NFSv3 and NFSv4 protocols. -//! -//! This implementation works by mounting the NFS export to a temporary -//! local directory via `mount -t nfs`, then performing standard -//! filesystem operations on the mount point. Requires the `nfs-utils` -//! package (or equivalent) and appropriate privileges for mount. - -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 std::process::Command; -use tokio::fs; -use tokio::io::AsyncReadExt; -use tracing::{debug, warn}; - -/// NFS version -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NfsVersion { - /// NFSv3 - V3, - /// NFSv4 - V4, - /// NFSv4.1 - 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 -/// -/// Mounts the NFS export to a temporary directory and performs -/// standard filesystem operations on the mount point. The mount -/// is created lazily on first use and cleaned up on drop. -pub struct NfsBackend { - id: String, - config: NfsConfig, - capabilities: StorageCapabilities, - /// Local mount point where the NFS export is mounted - mount_point: Option, -} - -impl NfsBackend { - /// Create a new NFS backend with the given configuration. - 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, - ..Default::default() - }, - mount_point: 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 to a temporary local directory. - /// - /// Creates a temp directory under `/tmp/cfk_nfs__` and - /// runs `mount -t nfs` to mount the export there. - pub async fn mount(&mut self) -> CfkResult<()> { - if self.mount_point.is_some() { - return Ok(()); // Already mounted - } - - let mount_dir = std::env::temp_dir().join(format!( - "cfk_nfs_{}_{}", self.id, std::process::id() - )); - - // Create mount point directory - std::fs::create_dir_all(&mount_dir).map_err(|e| { - CfkError::Other(format!("Failed to create mount point: {}", e)) - })?; - - self.mount_system(&mount_dir)?; - self.mount_point = Some(mount_dir); - Ok(()) - } - - /// Unmount the NFS export and clean up the temporary directory. - pub async fn unmount(&mut self) -> CfkResult<()> { - if let Some(ref mp) = self.mount_point { - self.unmount_system(mp)?; - let _ = std::fs::remove_dir(mp); - self.mount_point = None; - } - Ok(()) - } - - /// Ensure the export is mounted, mounting lazily if needed. - async fn ensure_mounted(&mut self) -> CfkResult<&Path> { - if self.mount_point.is_none() { - self.mount().await?; - } - Ok(self.mount_point.as_ref().unwrap().as_path()) - } - - /// Get the current mount point, returning an error if not mounted. - fn get_mount_point(&self) -> CfkResult<&Path> { - self.mount_point.as_deref().ok_or_else(|| { - CfkError::ProviderApi { - provider: "nfs".into(), - message: "NFS export is not mounted. Call mount() first.".into(), - } - }) - } - - /// Convert a VirtualPath to the real local path on the mount point. - fn to_real_path(&self, path: &VirtualPath) -> CfkResult { - let mp = self.get_mount_point()?; - let mut real = mp.to_path_buf(); - for seg in &path.segments { - real.push(seg); - } - Ok(real) - } - - /// Build an Entry from a local path on the mount point. - async fn entry_from_path(&self, real: &Path, vpath: &VirtualPath) -> CfkResult { - let meta = fs::metadata(real).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(Entry { path: vpath.clone(), kind, metadata }) - } - - /// Convert VirtualPath to NFS path components - fn to_path_components(&self, path: &VirtualPath) -> Vec { - path.segments.clone() - } -} - -#[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.mount_point.as_ref().map_or(false, |mp| mp.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())); - } - self.entry_from_path(&real, path).await - } - - 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(dir_entry) = read_dir.next_entry().await? { - let entry_path = dir_entry.path(); - let name = dir_entry.file_name().to_string_lossy().to_string(); - let vpath = path.join(&name); - if let Ok(entry) = self.entry_from_path(&entry_path, &vpath).await { - entries.push(entry); - } - } - - 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 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 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 copy(&self, _from: &VirtualPath, _to: &VirtualPath, _options: &CopyOptions) -> 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, options: &MoveOptions) -> CfkResult { - let src_real = self.to_real_path(from)?; - let dst_real = self.to_real_path(to)?; - - if !src_real.exists() { - return Err(CfkError::NotFound(from.to_string())); - } - if dst_real.exists() && !options.overwrite { - return Err(CfkError::AlreadyExists(to.to_string())); - } - - fs::rename(&src_real, &dst_real).await?; - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult { - #[cfg(unix)] - { - if let Some(ref mp) = self.mount_point { - use std::ffi::CString; - use std::mem::MaybeUninit; - - let path_cstr = CString::new(mp.to_string_lossy().as_bytes()) - .map_err(|_| CfkError::InvalidPath(mp.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; - - return Ok(SpaceInfo { - total: Some(total), - used: Some(used), - available: Some(available), - }); - } - } - } - - Ok(SpaceInfo::unknown()) - } -} - -impl Drop for NfsBackend { - fn drop(&mut self) { - // Best-effort unmount on drop - if let Some(ref mp) = self.mount_point { - let _ = self.unmount_system(mp); - let _ = std::fs::remove_dir(mp); - } - } -} - -/// NFS file types -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NfsFileType { - /// Regular file - Regular = 1, - /// Directory - Directory = 2, - /// Block device - BlockDevice = 3, - /// Character device - CharDevice = 4, - /// Symbolic link - Symlink = 5, - /// Socket - Socket = 6, - /// Named pipe (FIFO) - Fifo = 7, -} - -/// NFS file attributes (fattr3/fattr4) -#[derive(Debug, Clone, Default)] -pub struct NfsAttributes { - /// File type (see NfsFileType) - pub file_type: u32, - /// Unix permission mode - pub mode: u32, - /// Number of hard links - pub nlink: u32, - /// Owner user ID - pub uid: u32, - /// Owner group ID - pub gid: u32, - /// File size in bytes - pub size: u64, - /// Space used on disk - pub used: u64, - /// Filesystem ID - pub fsid: u64, - /// File ID (inode) - pub fileid: u64, - /// Access time seconds - pub atime_sec: u32, - /// Access time nanoseconds - pub atime_nsec: u32, - /// Modification time seconds - pub mtime_sec: u32, - /// Modification time nanoseconds - pub mtime_nsec: u32, - /// Change time seconds - pub ctime_sec: u32, - /// Change time nanoseconds - pub ctime_nsec: u32, -} - -impl NfsAttributes { - /// Convert NFS attributes to a CFK Entry. - 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(Permissions::new(self.mode)); - - 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<()> { - 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::Network(format!("Failed to run mount: {}", e)))?; - - 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<()> { - let status = Command::new("umount") - .arg(mount_point.to_str().unwrap_or("/mnt")) - .status() - .map_err(|e| CfkError::Network(format!("Failed to run umount: {}", e)))?; - - 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 2206bf5c..00000000 --- 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 82166eb0..00000000 --- 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 e19f9de5..00000000 --- a/czech-file-knife/cfk-providers/src/protocols.rs +++ /dev/null @@ -1,763 +0,0 @@ -// SPDX-License-Identifier: MPL-2.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}; -use std::io::{BufRead, BufReader, Read, Write}; -use std::net::TcpStream; -use std::time::Duration; - -/// Protocol capabilities -#[derive(Debug, Clone, Default)] -pub struct ProtocolInfo { - /// Human-readable protocol name - pub name: &'static str, - /// URI scheme identifier - pub scheme: &'static str, - /// Default TCP/UDP port - pub default_port: u16, - /// Whether the protocol uses encryption by default - pub encrypted: bool, - /// Whether the protocol supports uploading - pub bidirectional: bool, - /// Whether the protocol supports media streaming - pub streaming: bool, - /// Whether the protocol allows anonymous access - 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() -} - -/// Default TCP connect timeout -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15); - -/// Gopher protocol client -/// -/// Implements the Gopher protocol (RFC 1436). Gopher is a simple -/// text-based protocol: the client connects via TCP, sends a selector -/// string followed by CR-LF, and reads the server's response until -/// the connection closes. -pub mod gopher { - use super::*; - - /// Gopher item types (RFC 1436 Section 3.8) - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum ItemType { - /// Type 0: Plain text file - TextFile, - /// Type 1: Gopher directory/menu - Directory, - /// Type 2: CSO phone-book server - CsoServer, - /// Type 3: Error message - Error, - /// Type 4: BinHex-encoded file - BinHex, - /// Type 5: DOS binary archive - DosBinary, - /// Type 6: UUEncoded text - UuEncoded, - /// Type 7: Index-search server - IndexSearch, - /// Type 8: Telnet session pointer - Telnet, - /// Type 9: Binary file - Binary, - /// Type +: Redundant server - Mirror, - /// Type g: GIF image - Gif, - /// Type I: Generic image - Image, - /// Type h: HTML document - Html, - /// Type i: Informational message (displayed, not selectable) - Info, - /// Type s: Sound file - Sound, - } - - impl ItemType { - /// Parse a single-character item type code into an ItemType variant. - pub fn from_char(c: char) -> Option { - match c { - '0' => Some(Self::TextFile), - '1' => Some(Self::Directory), - '2' => Some(Self::CsoServer), - '3' => Some(Self::Error), - '4' => Some(Self::BinHex), - '5' => Some(Self::DosBinary), - '6' => Some(Self::UuEncoded), - '7' => Some(Self::IndexSearch), - '8' => Some(Self::Telnet), - '9' => Some(Self::Binary), - '+' => Some(Self::Mirror), - 'g' => Some(Self::Gif), - 'I' => Some(Self::Image), - 'h' => Some(Self::Html), - 'i' => Some(Self::Info), - 's' => Some(Self::Sound), - _ => None, - } - } - } - - /// A single entry in a Gopher directory listing - #[derive(Debug, Clone)] - pub struct GopherEntry { - /// Item type code - pub item_type: ItemType, - /// Display text shown to the user - pub display: String, - /// Selector string to fetch this item - pub selector: String, - /// Hostname of the server for this item - pub host: String, - /// Port of the server for this item - pub port: u16, - } - - /// Fetch a resource from a Gopher server. - /// - /// Parses a `gopher://host[:port][/type][selector]` URL. - /// Connects via TCP, sends the selector with CR-LF, and reads - /// the full response. - pub async fn fetch(url: &str) -> CfkResult> { - // Parse gopher URL - let url = url.strip_prefix("gopher://").unwrap_or(url); - - // Split host[:port] from selector - let (hostport, selector) = match url.find('/') { - Some(idx) => (&url[..idx], &url[idx + 1..]), - None => (url, ""), - }; - - let (host, port) = match hostport.rsplit_once(':') { - Some((h, p)) => (h, p.parse::().unwrap_or(70)), - None => (hostport, 70), - }; - - // The first character after / may be a type indicator; skip it - // for the selector sent to the server - let selector = if selector.len() > 1 && selector.as_bytes()[0].is_ascii_alphanumeric() { - // First char is the type indicator (0-9, g, h, I, etc.) - &selector[1..] - } else { - selector - }; - - let addr = format!("{}:{}", host, port); - let mut stream = TcpStream::connect_timeout( - &addr.parse().map_err(|e| CfkError::Network(format!("Invalid address: {}", e)))?, - DEFAULT_TIMEOUT, - ).map_err(|e| CfkError::Network(format!("Gopher connect failed: {}", e)))?; - - stream.set_read_timeout(Some(DEFAULT_TIMEOUT)) - .map_err(|e| CfkError::Network(format!("Set timeout failed: {}", e)))?; - - // Send selector + CRLF - write!(stream, "{}\r\n", selector) - .map_err(|e| CfkError::Network(format!("Gopher send failed: {}", e)))?; - - // Read entire response - let mut response = Vec::new(); - stream.read_to_end(&mut response) - .map_err(|e| CfkError::Network(format!("Gopher read failed: {}", e)))?; - - Ok(response) - } - - /// Parse a Gopher directory listing (type 1 response). - /// - /// Each line in a Gopher menu has the format: - /// `\t\t\t\r\n` - /// - /// The listing is terminated by a line containing only `.` - pub fn parse_directory(data: &[u8]) -> Vec { - let text = String::from_utf8_lossy(data); - let mut entries = Vec::new(); - - for line in text.lines() { - // End-of-listing marker - if line == "." { - break; - } - - if line.is_empty() { - continue; - } - - // First character is the item type - let type_char = line.chars().next().unwrap_or('i'); - let item_type = match ItemType::from_char(type_char) { - Some(t) => t, - None => continue, // Skip unknown types - }; - - // Rest of line is tab-separated: display, selector, host, port - let rest = &line[type_char.len_utf8()..]; - let fields: Vec<&str> = rest.split('\t').collect(); - - if fields.len() < 4 { - // Malformed line; try to salvage what we can - entries.push(GopherEntry { - item_type, - display: fields.first().unwrap_or(&"").to_string(), - selector: fields.get(1).unwrap_or(&"").to_string(), - host: fields.get(2).unwrap_or(&"").to_string(), - port: fields.get(3).and_then(|p| p.trim().parse().ok()).unwrap_or(70), - }); - continue; - } - - entries.push(GopherEntry { - item_type, - display: fields[0].to_string(), - selector: fields[1].to_string(), - host: fields[2].to_string(), - port: fields[3].trim().parse().unwrap_or(70), - }); - } - - entries - } -} - -/// Gemini protocol client -/// -/// Implements the Gemini protocol (gemini://geminiprotocol.net/). -/// Gemini uses TLS on port 1965. The client sends the full URL -/// followed by CR-LF, and the server responds with a status line -/// (` \r\n`) followed by the body. -pub mod gemini { - use super::*; - - /// Gemini response status code categories - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Status { - /// 1x: Input required from the user - Input = 10, - /// 2x: Success — body follows - Success = 20, - /// 3x: Redirect — follow the URL in meta - Redirect = 30, - /// 4x: Temporary failure - TemporaryFailure = 40, - /// 5x: Permanent failure - PermanentFailure = 50, - /// 6x: Client certificate required - ClientCertRequired = 60, - } - - impl Status { - /// Parse a two-digit status code into a Status category. - pub fn from_code(code: u8) -> Option { - match code / 10 { - 1 => Some(Self::Input), - 2 => Some(Self::Success), - 3 => Some(Self::Redirect), - 4 => Some(Self::TemporaryFailure), - 5 => Some(Self::PermanentFailure), - 6 => Some(Self::ClientCertRequired), - _ => None, - } - } - } - - /// Fetch a Gemini URL and return (status, meta, body). - /// - /// Connects via TLS to the server on port 1965, sends the URL, - /// and parses the response. Uses native-tls with certificate - /// verification disabled (Gemini servers commonly use self-signed - /// certificates — this is by design in the Gemini protocol). - pub async fn fetch(url: &str) -> CfkResult<(Status, String, Vec)> { - // Parse the URL - let url_str = if url.starts_with("gemini://") { - url.to_string() - } else { - format!("gemini://{}", url) - }; - - let parsed = url::Url::parse(&url_str) - .map_err(|e| CfkError::InvalidPath(format!("Invalid Gemini URL: {}", e)))?; - - let host = parsed.host_str() - .ok_or_else(|| CfkError::InvalidPath("Missing host in Gemini URL".into()))?; - let port = parsed.port().unwrap_or(1965); - let addr = format!("{}:{}", host, port); - - // Connect via TCP - let tcp_stream = TcpStream::connect_timeout( - &addr.parse().map_err(|e| CfkError::Network(format!("Invalid address: {}", e)))?, - DEFAULT_TIMEOUT, - ).map_err(|e| CfkError::Network(format!("Gemini TCP connect failed: {}", e)))?; - - tcp_stream.set_read_timeout(Some(DEFAULT_TIMEOUT)) - .map_err(|e| CfkError::Network(format!("Set timeout failed: {}", e)))?; - - // Set up TLS — Gemini uses self-signed certs commonly, - // so we accept invalid certificates (TOFU model) - let connector = native_tls::TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - .map_err(|e| CfkError::Network(format!("TLS setup failed: {}", e)))?; - - let mut tls_stream = connector.connect(host, tcp_stream) - .map_err(|e| CfkError::Network(format!("TLS handshake failed: {}", e)))?; - - // Send the URL + CRLF - write!(tls_stream, "{}\r\n", url_str) - .map_err(|e| CfkError::Network(format!("Gemini send failed: {}", e)))?; - - // Read the response header (first line: " \r\n") - let mut reader = BufReader::new(&mut tls_stream); - let mut header_line = String::new(); - reader.read_line(&mut header_line) - .map_err(|e| CfkError::Network(format!("Gemini header read failed: {}", e)))?; - - let header_line = header_line.trim_end_matches('\n').trim_end_matches('\r'); - - // Parse status code (first two characters) - if header_line.len() < 2 { - return Err(CfkError::ProviderApi { - provider: "gemini".into(), - message: "Response header too short".into(), - }); - } - - let status_code: u8 = header_line[..2].parse().map_err(|_| { - CfkError::ProviderApi { - provider: "gemini".into(), - message: format!("Invalid status code: {}", &header_line[..2]), - } - })?; - - let status = Status::from_code(status_code).ok_or_else(|| { - CfkError::ProviderApi { - provider: "gemini".into(), - message: format!("Unknown status category: {}", status_code), - } - })?; - - // Meta is everything after the status code and space - let meta = if header_line.len() > 3 { - header_line[3..].to_string() - } else { - String::new() - }; - - // Read the body (only present for 2x responses) - let mut body = Vec::new(); - if status == Status::Success { - reader.read_to_end(&mut body) - .map_err(|e| CfkError::Network(format!("Gemini body read failed: {}", e)))?; - } - - Ok((status, meta, body)) - } -} - -/// NNTP (Network News Transfer Protocol) client -/// -/// Implements basic NNTP commands (RFC 3977). NNTP is a text-based -/// protocol where the client sends commands and the server responds -/// with a three-digit status code followed by text. -pub mod nntp { - use super::*; - - /// A Usenet article - #[derive(Debug, Clone)] - pub struct Article { - /// Message-ID header - pub message_id: String, - /// Subject header - pub subject: String, - /// From header - pub from: String, - /// Date header - pub date: String, - /// List of newsgroups this article was posted to - pub newsgroups: Vec, - /// Article body text - pub body: String, - } - - /// An active NNTP connection to a Usenet server - pub struct NntpClient { - reader: BufReader, - writer: TcpStream, - } - - impl NntpClient { - /// Read a single response line from the server. - fn read_line(&mut self) -> CfkResult { - let mut line = String::new(); - self.reader.read_line(&mut line) - .map_err(|e| CfkError::Network(format!("NNTP read failed: {}", e)))?; - Ok(line.trim_end_matches('\n').trim_end_matches('\r').to_string()) - } - - /// Read a multi-line response terminated by a line containing only `.`. - fn read_multiline(&mut self) -> CfkResult> { - let mut lines = Vec::new(); - loop { - let line = self.read_line()?; - if line == "." { - break; - } - // Dot-stuffing: lines starting with ".." have the leading dot removed - let content = if line.starts_with("..") { - line[1..].to_string() - } else { - line - }; - lines.push(content); - } - Ok(lines) - } - - /// Send a command to the server. - fn send_command(&mut self, cmd: &str) -> CfkResult<()> { - write!(self.writer, "{}\r\n", cmd) - .map_err(|e| CfkError::Network(format!("NNTP send failed: {}", e))) - } - - /// Send a command and read the response status line. - fn command(&mut self, cmd: &str) -> CfkResult<(u16, String)> { - self.send_command(cmd)?; - let response = self.read_line()?; - parse_response(&response) - } - - /// Select a newsgroup. - /// - /// Returns (estimated_count, low_watermark, high_watermark). - pub fn group(&mut self, name: &str) -> CfkResult<(u64, u64, u64)> { - let (code, text) = self.command(&format!("GROUP {}", name))?; - if code != 211 { - return Err(CfkError::ProviderApi { - provider: "nntp".into(), - message: format!("GROUP failed ({}): {}", code, text), - }); - } - - // Response: "211 count low high groupname" - let parts: Vec<&str> = text.split_whitespace().collect(); - if parts.len() < 3 { - return Err(CfkError::ProviderApi { - provider: "nntp".into(), - message: "Malformed GROUP response".into(), - }); - } - - let count = parts[0].parse::().unwrap_or(0); - let low = parts[1].parse::().unwrap_or(0); - let high = parts[2].parse::().unwrap_or(0); - - Ok((count, low, high)) - } - - /// Retrieve an article by its number or message-id. - pub fn article(&mut self, id: &str) -> CfkResult
{ - let (code, _text) = self.command(&format!("ARTICLE {}", id))?; - if code != 220 { - return Err(CfkError::NotFound(format!("Article not found: {}", id))); - } - - // Read multi-line response (headers + blank line + body) - let lines = self.read_multiline()?; - - let mut message_id = String::new(); - let mut subject = String::new(); - let mut from = String::new(); - let mut date = String::new(); - let mut newsgroups = Vec::new(); - let mut in_body = false; - let mut body_lines = Vec::new(); - - for line in &lines { - if in_body { - body_lines.push(line.as_str()); - continue; - } - - if line.is_empty() { - in_body = true; - continue; - } - - // Parse headers (case-insensitive) - let lower = line.to_lowercase(); - if let Some(_) = lower.strip_prefix("message-id:") { - message_id = line[11..].trim().to_string(); - } else if let Some(_) = lower.strip_prefix("subject:") { - subject = line[8..].trim().to_string(); - } else if let Some(_) = lower.strip_prefix("from:") { - from = line[5..].trim().to_string(); - } else if let Some(_) = lower.strip_prefix("date:") { - date = line[5..].trim().to_string(); - } else if let Some(_) = lower.strip_prefix("newsgroups:") { - let groups_str = line[11..].trim(); - newsgroups = groups_str.split(',').map(|g| g.trim().to_string()).collect(); - } - } - - Ok(Article { - message_id, - subject, - from, - date, - newsgroups, - body: body_lines.join("\n"), - }) - } - - /// List active newsgroups matching an optional wildmat pattern. - pub fn list_active(&mut self, wildmat: Option<&str>) -> CfkResult> { - let cmd = match wildmat { - Some(pat) => format!("LIST ACTIVE {}", pat), - None => "LIST ACTIVE".to_string(), - }; - let (code, _text) = self.command(&cmd)?; - if code != 215 { - return Err(CfkError::ProviderApi { - provider: "nntp".into(), - message: "LIST ACTIVE failed".into(), - }); - } - - let lines = self.read_multiline()?; - let groups: Vec = lines.iter() - .filter_map(|line| { - line.split_whitespace().next().map(String::from) - }) - .collect(); - - Ok(groups) - } - - /// Quit the NNTP session cleanly. - pub fn quit(&mut self) -> CfkResult<()> { - let _ = self.command("QUIT"); - Ok(()) - } - } - - /// Parse an NNTP response line into (status_code, rest_of_line). - fn parse_response(line: &str) -> CfkResult<(u16, String)> { - if line.len() < 3 { - return Err(CfkError::ProviderApi { - provider: "nntp".into(), - message: format!("Response too short: {}", line), - }); - } - - let code: u16 = line[..3].parse().map_err(|_| { - CfkError::ProviderApi { - provider: "nntp".into(), - message: format!("Invalid status code: {}", &line[..3]), - } - })?; - - let text = if line.len() > 4 { line[4..].to_string() } else { String::new() }; - Ok((code, text)) - } - - /// Connect to an NNTP server and return a client handle. - /// - /// Reads and validates the server greeting (200 or 201). - /// The `_tls` parameter is currently unused; for NNTPS, use port 563 - /// with a TLS wrapper externally. - pub async fn connect(host: &str, port: u16, _tls: bool) -> CfkResult { - let addr = format!("{}:{}", host, port); - let stream = TcpStream::connect_timeout( - &addr.parse().map_err(|e| CfkError::Network(format!("Invalid address: {}", e)))?, - DEFAULT_TIMEOUT, - ).map_err(|e| CfkError::Network(format!("NNTP connect failed: {}", e)))?; - - stream.set_read_timeout(Some(DEFAULT_TIMEOUT)) - .map_err(|e| CfkError::Network(format!("Set timeout failed: {}", e)))?; - - let writer = stream.try_clone() - .map_err(|e| CfkError::Network(format!("Failed to clone socket: {}", e)))?; - - let mut client = NntpClient { - reader: BufReader::new(stream), - writer, - }; - - // Read server greeting - let greeting = client.read_line()?; - let (code, _text) = parse_response(&greeting)?; - - // 200 = posting allowed, 201 = posting not allowed, both are OK - if code != 200 && code != 201 { - return Err(CfkError::ProviderApi { - provider: "nntp".into(), - message: format!("Unexpected greeting: {}", greeting), - }); - } - - Ok(client) - } - - /// Convenience function to list newsgroups from a server. - pub async fn list_groups(host: &str, port: u16) -> CfkResult> { - let mut client = connect(host, port, false).await?; - let groups = client.list_active(None)?; - let _ = client.quit(); - Ok(groups) - } -} 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 b634e168..00000000 --- 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 f271cbec..00000000 --- a/czech-file-knife/cfk-providers/src/sftp.rs +++ /dev/null @@ -1,604 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! SFTP storage backend -//! -//! SSH File Transfer Protocol implementation. -//! Supports password, key-based, and agent authentication. -//! -//! Delegates to the system `sftp` / `ssh` CLI tools via -//! `std::process::Command`. Falls back gracefully when the tools -//! are not available. - -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::PathBuf; -use std::process::Command; -use tracing::{debug, warn}; - -/// 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 -/// -/// Delegates to the system `sftp` CLI for file operations. -/// Uses batch mode (`-b -`) to pipe commands via stdin. -pub struct SftpBackend { - id: String, - config: SftpConfig, - capabilities: StorageCapabilities, -} - -impl SftpBackend { - /// Create a new SFTP backend with the given configuration. - 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, - ..Default::default() - }, - } - } - - /// 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 on the SFTP server. - 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("/")) - } - } - - /// Get the username from the configured auth method. - fn username(&self) -> &str { - match &self.config.auth { - SftpAuth::Password { username, .. } => username, - SftpAuth::PrivateKey { username, .. } => username, - SftpAuth::Agent { username } => username, - } - } - - /// Build common SSH options for the sftp command. - fn ssh_options(&self) -> Vec { - let mut opts = Vec::new(); - - // Port - opts.push("-oPort".to_string()); - opts.push(self.config.port.to_string()); - - // Batch mode — disable interactive prompts - opts.push("-oBatchMode=yes".to_string()); - - // Host key checking - if self.config.skip_host_key_check { - opts.push("-oStrictHostKeyChecking=no".to_string()); - opts.push("-oUserKnownHostsFile=/dev/null".to_string()); - } else if let Some(ref kh) = self.config.known_hosts { - opts.push(format!("-oUserKnownHostsFile={}", kh.display())); - } - - // Private key - if let SftpAuth::PrivateKey { private_key_path, .. } = &self.config.auth { - opts.push("-oIdentityFile".to_string()); - opts.push(private_key_path.display().to_string()); - } - - opts - } - - /// Run an sftp batch command and return its stdout. - /// - /// The `batch_commands` string is fed to `sftp -b -` via stdin. - fn run_sftp_batch(&self, batch_commands: &str) -> CfkResult { - let destination = format!("{}@{}", self.username(), self.config.host); - let mut args = vec!["-b".to_string(), "-".to_string()]; - args.extend(self.ssh_options()); - args.push(destination); - - debug!(batch = %batch_commands, "Running sftp batch command"); - - let output = Command::new("sftp") - .args(&args) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - .and_then(|mut child| { - use std::io::Write; - if let Some(ref mut stdin) = child.stdin { - stdin.write_all(batch_commands.as_bytes())?; - } - child.wait_with_output() - }) - .map_err(|e| CfkError::Network(format!("Failed to run sftp: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(CfkError::ProviderApi { - provider: "sftp".into(), - message: format!("sftp failed: {}", stderr.trim()), - }); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - /// Run an SSH command and return its stdout (used for stat, ls). - fn run_ssh_command(&self, remote_cmd: &str) -> CfkResult { - let destination = format!("{}@{}", self.username(), self.config.host); - - let mut args = Vec::new(); - // Port - args.push("-p".to_string()); - args.push(self.config.port.to_string()); - - // Batch mode - args.push("-oBatchMode=yes".to_string()); - - if self.config.skip_host_key_check { - args.push("-oStrictHostKeyChecking=no".to_string()); - args.push("-oUserKnownHostsFile=/dev/null".to_string()); - } else if let Some(ref kh) = self.config.known_hosts { - args.push(format!("-oUserKnownHostsFile={}", kh.display())); - } - - if let SftpAuth::PrivateKey { private_key_path, .. } = &self.config.auth { - args.push("-i".to_string()); - args.push(private_key_path.display().to_string()); - } - - args.push(destination); - args.push(remote_cmd.to_string()); - - let output = Command::new("ssh") - .args(&args) - .output() - .map_err(|e| CfkError::Network(format!("Failed to run ssh: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(CfkError::ProviderApi { - provider: "sftp".into(), - message: format!("ssh command failed: {}", stderr.trim()), - }); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - /// Connect to SFTP server (validates connectivity). - pub async fn connect(&self) -> CfkResult<()> { - self.run_sftp_batch("ls\n")?; - Ok(()) - } -} - -#[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 { - // Check that the sftp binary exists - Command::new("sftp").arg("-h").output().is_ok() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let remote_path = self.to_remote_path(path); - - // Use ssh + stat to get file metadata - let cmd = format!( - "stat -c '%F %s %Y %a' '{}'", - remote_path.replace('\'', "'\\''") - ); - let output = self.run_ssh_command(&cmd)?; - let trimmed = output.trim(); - - // Parse: " " - let parts: Vec<&str> = trimmed.splitn(4, ' ').collect(); - if parts.len() < 4 { - return Err(CfkError::NotFound(path.to_string())); - } - - let kind = if parts[0].contains("directory") { - EntryKind::Directory - } else if parts[0].contains("symbolic") { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let mut metadata = Metadata::new(); - if let Ok(size) = parts[1].parse::() { - metadata.size = Some(size); - } - if let Ok(mtime) = parts[2].parse::() { - metadata.modified = chrono::DateTime::from_timestamp(mtime, 0); - } - if let Ok(mode) = u32::from_str_radix(parts[3], 8) { - metadata.permissions = Some(Permissions::new(mode)); - } - - Ok(Entry { path: path.clone(), kind, metadata }) - } - - async fn list_directory(&self, path: &VirtualPath, _options: &ListOptions) -> CfkResult { - let remote_path = self.to_remote_path(path); - - // Use ssh + ls to get a reliable directory listing - let cmd = format!( - "ls -1a '{}'", - remote_path.replace('\'', "'\\''") - ); - let output = self.run_ssh_command(&cmd)?; - - let mut entries = Vec::new(); - for line in output.lines() { - let name = line.trim(); - if name.is_empty() || name == "." || name == ".." { - continue; - } - - // We get basic type info from a second stat call per entry - // For performance, use ls -la and parse instead - let entry_path = path.join(name); - entries.push(Entry { - path: entry_path, - kind: EntryKind::Unknown, // Could be enriched with stat - metadata: Metadata::new(), - }); - } - - // Enrich entries with type info via a single stat call - if !entries.is_empty() { - let stat_cmd = format!( - "for f in '{}'/*; do stat -c '%F %s %Y %n' \"$f\" 2>/dev/null; done", - remote_path.replace('\'', "'\\''") - ); - if let Ok(stat_output) = self.run_ssh_command(&stat_cmd) { - // Rebuild entries from stat output - entries.clear(); - for line in stat_output.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - - // Format: " " - let parts: Vec<&str> = trimmed.splitn(4, ' ').collect(); - if parts.len() < 4 { - continue; - } - - let kind = if parts[0].contains("directory") { - EntryKind::Directory - } else if parts[0].contains("symbolic") { - EntryKind::Symlink - } else { - EntryKind::File - }; - - let full_remote_path = parts[3]; - let name = full_remote_path - .rsplit('/') - .next() - .unwrap_or(full_remote_path); - - if name == "." || name == ".." { - continue; - } - - let mut metadata = Metadata::new(); - if let Ok(size) = parts[1].parse::() { - metadata.size = Some(size); - } - if let Ok(mtime) = parts[2].parse::() { - metadata.modified = chrono::DateTime::from_timestamp(mtime, 0); - } - - let entry_path = path.join(name); - entries.push(Entry { path: entry_path, kind, metadata }); - } - } - } - - Ok(DirectoryListing::new(path.clone(), entries)) - } - - async fn read_file(&self, path: &VirtualPath, _options: &ReadOptions) -> CfkResult { - let remote_path = self.to_remote_path(path); - - // Download to a temp file via sftp, then read locally - let tmp_dir = std::env::temp_dir(); - let tmp_file = tmp_dir.join(format!("cfk_sftp_{}", std::process::id())); - let tmp_path_str = tmp_file.display().to_string(); - - let batch = format!("get {} {}\n", remote_path, tmp_path_str); - self.run_sftp_batch(&batch)?; - - let data = std::fs::read(&tmp_file).map_err(|e| { - CfkError::Other(format!("Failed to read downloaded file: {}", e)) - })?; - let _ = std::fs::remove_file(&tmp_file); - - let bytes = Bytes::from(data); - Ok(Box::pin(futures::stream::once(async { Ok(bytes) }))) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes, _options: &WriteOptions) -> CfkResult { - let remote_path = self.to_remote_path(path); - - // Write to a temp file, then upload via sftp - let tmp_dir = std::env::temp_dir(); - let tmp_file = tmp_dir.join(format!("cfk_sftp_put_{}", std::process::id())); - std::fs::write(&tmp_file, &data).map_err(|e| { - CfkError::Other(format!("Failed to write temp file: {}", e)) - })?; - - let tmp_path_str = tmp_file.display().to_string(); - let batch = format!("put {} {}\n", tmp_path_str, remote_path); - let result = self.run_sftp_batch(&batch); - let _ = std::fs::remove_file(&tmp_file); - result?; - - let mut metadata = Metadata::new(); - metadata.size = Some(data.len() as u64); - Ok(Entry { - path: path.clone(), - kind: EntryKind::File, - metadata, - }) - } - - 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 delete(&self, path: &VirtualPath, _options: &DeleteOptions) -> CfkResult<()> { - let remote_path = self.to_remote_path(path); - - // Try rm first (file), then rmdir (directory) - let batch = format!("rm {}\n", remote_path); - match self.run_sftp_batch(&batch) { - Ok(_) => Ok(()), - Err(_) => { - let batch = format!("rmdir {}\n", remote_path); - self.run_sftp_batch(&batch)?; - Ok(()) - } - } - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let remote_path = self.to_remote_path(path); - let batch = format!("mkdir {}\n", remote_path); - self.run_sftp_batch(&batch)?; - - Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::new(), - }) - } - - async fn copy(&self, _from: &VirtualPath, _to: &VirtualPath, _options: &CopyOptions) -> CfkResult { - // SFTP doesn't support server-side copy - Err(CfkError::Unsupported( - "SFTP doesn't support native copy".into(), - )) - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath, _options: &MoveOptions) -> CfkResult { - let from_path = self.to_remote_path(from); - let to_path = self.to_remote_path(to); - - let batch = format!("rename {} {}\n", from_path, to_path); - self.run_sftp_batch(&batch)?; - - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult { - let remote_path = self.to_remote_path(&VirtualPath::root(&self.id)); - - // Use ssh + df to get space info - let cmd = format!("df -B1 '{}' | tail -1", remote_path.replace('\'', "'\\''")); - if let Ok(output) = self.run_ssh_command(&cmd) { - let parts: Vec<&str> = output.trim().split_whitespace().collect(); - if parts.len() >= 4 { - let total = parts[1].parse::().ok(); - let used = parts[2].parse::().ok(); - let available = parts[3].parse::().ok(); - return Ok(SpaceInfo { total, used, available }); - } - } - - Ok(SpaceInfo::unknown()) - } -} - -/// Helper to get username from environment -mod whoami { - /// Returns the current username from the USER or USERNAME environment variable. - 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 { - /// File size in bytes - pub size: Option, - /// Owner user ID - pub uid: Option, - /// Owner group ID - pub gid: Option, - /// Unix permission bits - pub permissions: Option, - /// Last access time (Unix epoch) - pub atime: Option, - /// Last modification time (Unix epoch) - pub mtime: Option, -} - -impl FileAttributes { - /// Returns true if the entry is a directory. - pub fn is_dir(&self) -> bool { - self.permissions - .map(|p| (p & 0o40000) != 0) - .unwrap_or(false) - } - - /// Returns true if the entry is a symbolic link. - pub fn is_symlink(&self) -> bool { - self.permissions - .map(|p| (p & 0o120000) == 0o120000) - .unwrap_or(false) - } - - /// Returns true if the entry is a regular file. - pub fn is_file(&self) -> bool { - self.permissions - .map(|p| (p & 0o100000) != 0) - .unwrap_or(false) - } - - /// Convert to CFK Metadata. - pub fn to_metadata(&self) -> Metadata { - let mut meta = Metadata::default(); - meta.size = self.size; - if let Some(mode) = self.permissions { - meta.permissions = Some(Permissions::new(mode)); - } - - 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 ebd82176..00000000 --- a/czech-file-knife/cfk-providers/src/smb.rs +++ /dev/null @@ -1,781 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! SMB/CIFS storage backend -//! -//! Server Message Block / Common Internet File System protocol. -//! Compatible with Windows shares, Samba, and macOS file sharing. -//! -//! This implementation delegates to the system `smbclient` CLI tool -//! via `std::process::Command`. Falls back gracefully if smbclient -//! is not installed. - -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::PathBuf; -use std::process::Command; -use tracing::{debug, warn}; - -/// 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 -/// -/// Delegates to system `smbclient` CLI for all file operations. -/// Falls back gracefully when smbclient is unavailable. -pub struct SmbBackend { - id: String, - config: SmbConfig, - capabilities: StorageCapabilities, - session: Option, - tree_id: Option, -} - -impl SmbBackend { - /// Create a new SMB backend instance with the given configuration. - 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 - ..Default::default() - }; - - // Adjust capabilities based on version - if config.version == SmbVersion::Smb1 { - caps.copy = false; // SMB1 doesn't have server-side copy - } - - 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() - }, - )) - } - - /// Check whether the `smbclient` binary is available on the system PATH. - fn smbclient_available() -> bool { - Command::new("smbclient") - .arg("--version") - .output() - .is_ok() - } - - /// Build common smbclient authentication arguments from the backend configuration. - fn auth_args(&self) -> Vec { - match &self.config.auth { - SmbAuth::Anonymous => vec!["-N".to_string()], - SmbAuth::Ntlm { username, password, domain } => { - let mut args = vec![ - "-U".to_string(), - if let Some(dom) = domain { - format!("{}\\{}%{}", dom, username, password) - } else { - format!("{}%{}", username, password) - }, - ]; - args - } - SmbAuth::Kerberos { .. } => vec!["-k".to_string()], - } - } - - /// Build the smbclient service string (e.g. `//server/share`). - fn service_string(&self) -> String { - format!("//{}/{}", self.config.server, self.config.share) - } - - /// Build the SMB protocol version flag for smbclient. - fn max_protocol_arg(&self) -> Vec { - let proto = match self.config.version { - SmbVersion::Smb1 => "NT1", - SmbVersion::Smb2 => "SMB2", - SmbVersion::Smb21 => "SMB2", - SmbVersion::Smb3 | SmbVersion::Smb302 | SmbVersion::Smb311 => "SMB3", - }; - vec!["-m".to_string(), proto.to_string()] - } - - /// Run an smbclient command with the given `-c` directive and return stdout. - fn run_smbclient_command(&self, smb_command: &str) -> CfkResult { - if !Self::smbclient_available() { - return Err(CfkError::Unsupported( - "smbclient is not installed. Install samba-client to use the SMB backend.".into(), - )); - } - - let mut args = vec![self.service_string()]; - args.extend(self.auth_args()); - args.extend(self.max_protocol_arg()); - args.push("-p".to_string()); - args.push(self.config.port.to_string()); - args.push("-c".to_string()); - args.push(smb_command.to_string()); - - debug!(cmd = %smb_command, "Running smbclient command"); - - let output = Command::new("smbclient") - .args(&args) - .output() - .map_err(|e| CfkError::Network(format!("Failed to run smbclient: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(CfkError::ProviderApi { - provider: "smb".into(), - message: format!("smbclient failed: {}", stderr.trim()), - }); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - /// Connect to SMB server (validates connectivity via smbclient) - pub async fn connect(&mut self) -> CfkResult<()> { - // Verify smbclient can reach the share by listing root - let _output = self.run_smbclient_command("ls")?; - self.session = Some(SessionId(1)); - self.tree_id = Some(TreeId(1)); - Ok(()) - } - - /// Disconnect from SMB server - pub async fn disconnect(&mut self) -> CfkResult<()> { - 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("\\")) - } - } - - /// Convert VirtualPath to a POSIX-style path for smbclient commands. - fn to_smb_posix_path(&self, path: &VirtualPath) -> String { - if path.segments.is_empty() { - "/".to_string() - } else { - format!("/{}", path.segments.join("/")) - } - } - - /// Parse an `ls` output line from smbclient into an entry. - /// - /// Typical smbclient `ls` output lines look like: - /// ```text - /// . D 0 Mon Jan 1 00:00:00 2024 - /// .. D 0 Mon Jan 1 00:00:00 2024 - /// somefile.txt A 1234 Mon Jan 1 12:34:56 2024 - /// subdir D 0 Mon Jan 1 00:00:00 2024 - /// ``` - fn parse_ls_line(&self, line: &str, parent_path: &VirtualPath) -> Option { - let trimmed = line.trim(); - if trimmed.is_empty() { - return None; - } - - // smbclient ls format: " name attrs size date" - // We parse by finding the attributes column (single letter flags like D, A, H, S, R, N) - // The attributes are typically a sequence of letters like "D", "A", "DA", "AHS" etc. - // They appear after whitespace following the filename. - - // Skip summary lines like "blocks of size ... blocks available" - if trimmed.contains("blocks of size") || trimmed.contains("blocks available") { - return None; - } - - // Try to parse: filename is at the start (may have spaces), - // then flags, then size, then date. - // We look for the attributes field which contains only D/A/H/S/R/N characters. - // The trick is to scan from the right: date, then size, then attrs, then name. - - // Split into parts respecting the fixed-width format - // smbclient uses a fixed-width column format: - // filename (variable) attrs(~1-6 chars) size(~10 chars right-aligned) date - - // Find the size field (a number) by scanning from the right past the date - let parts: Vec<&str> = trimmed.splitn(2, |c: char| c == 'D' || c == 'A' || c == 'N' || c == 'H' || c == 'S' || c == 'R') - .collect(); - - // Simpler approach: use a regex-like manual parse - // Pattern: " NAME FLAGS SIZE DATE" - // FLAGS is one or more of [DAHSRN] - // SIZE is a number - // We find the last number before the date - - // Find all whitespace-separated tokens - let tokens: Vec<&str> = trimmed.split_whitespace().collect(); - if tokens.len() < 6 { - return None; - } - - // The name is everything before the attribute flags - // Attribute flags are single-char sequences from DAHSRN - // Find the attribute token index - let mut attr_idx = None; - for (i, token) in tokens.iter().enumerate() { - if i > 0 && token.chars().all(|c| "DAHSRN".contains(c)) && !token.is_empty() { - // Next token should be a number (size) - if i + 1 < tokens.len() && tokens[i + 1].parse::().is_ok() { - attr_idx = Some(i); - break; - } - } - } - - let attr_idx = attr_idx?; - let name = tokens[..attr_idx].join(" "); - - // Skip . and .. - if name == "." || name == ".." { - return None; - } - - let attrs = tokens[attr_idx]; - let size: u64 = tokens[attr_idx + 1].parse().ok()?; - - let is_dir = attrs.contains('D'); - let kind = if is_dir { EntryKind::Directory } else { EntryKind::File }; - - let mut metadata = Metadata::new(); - metadata.size = Some(size); - metadata.custom.insert("smb_attrs".to_string(), attrs.to_string()); - - let entry_path = parent_path.join(&name); - Some(Entry { path: entry_path, kind, metadata }) - } -} - -#[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::smbclient_available() - } - - async fn get_metadata(&self, path: &VirtualPath) -> CfkResult { - let smb_path = self.to_smb_posix_path(path); - let cmd = format!("allinfo \"{}\"", smb_path); - let output = self.run_smbclient_command(&cmd)?; - - // Parse allinfo output for file attributes - let mut metadata = Metadata::new(); - let mut kind = EntryKind::File; - - for line in output.lines() { - let trimmed = line.trim(); - if let Some(size_str) = trimmed.strip_prefix("stream: [::$DATA], ") { - // Parse size from stream info - if let Some(size_part) = size_str.strip_suffix(" bytes") { - if let Ok(size) = size_part.trim().parse::() { - metadata.size = Some(size); - } - } - } - if trimmed.starts_with("attributes:") && trimmed.contains("D") { - kind = EntryKind::Directory; - } - } - - Ok(Entry { path: path.clone(), kind, metadata }) - } - - async fn list_directory(&self, path: &VirtualPath, _options: &ListOptions) -> CfkResult { - let smb_path = self.to_smb_posix_path(path); - let cmd = if smb_path == "/" { - "ls".to_string() - } else { - format!("ls \"{}/*\"", smb_path.trim_end_matches('/')) - }; - - let output = self.run_smbclient_command(&cmd)?; - - let mut entries = Vec::new(); - for line in output.lines() { - if let Some(entry) = self.parse_ls_line(line, path) { - entries.push(entry); - } - } - - Ok(DirectoryListing::new(path.clone(), entries)) - } - - async fn read_file(&self, path: &VirtualPath, _options: &ReadOptions) -> CfkResult { - let smb_path = self.to_smb_posix_path(path); - - // Use a temporary file to download via smbclient - let tmp_dir = std::env::temp_dir(); - let tmp_file = tmp_dir.join(format!("cfk_smb_{}", std::process::id())); - let tmp_path_str = tmp_file.display().to_string(); - - let cmd = format!("get \"{}\" \"{}\"", smb_path, tmp_path_str); - self.run_smbclient_command(&cmd)?; - - // Read the temporary file and clean up - let data = std::fs::read(&tmp_file).map_err(|e| { - CfkError::Other(format!("Failed to read downloaded file: {}", e)) - })?; - let _ = std::fs::remove_file(&tmp_file); - - let bytes = Bytes::from(data); - Ok(Box::pin(futures::stream::once(async { Ok(bytes) }))) - } - - async fn write_file(&self, path: &VirtualPath, data: Bytes, _options: &WriteOptions) -> CfkResult { - let smb_path = self.to_smb_posix_path(path); - - // Write data to a temporary local file, then upload via smbclient - let tmp_dir = std::env::temp_dir(); - let tmp_file = tmp_dir.join(format!("cfk_smb_put_{}", std::process::id())); - std::fs::write(&tmp_file, &data).map_err(|e| { - CfkError::Other(format!("Failed to write temp file: {}", e)) - })?; - - let tmp_path_str = tmp_file.display().to_string(); - let cmd = format!("put \"{}\" \"{}\"", tmp_path_str, smb_path); - let result = self.run_smbclient_command(&cmd); - let _ = std::fs::remove_file(&tmp_file); - result?; - - // Return metadata for the written file - let mut metadata = Metadata::new(); - metadata.size = Some(data.len() as u64); - Ok(Entry { - path: path.clone(), - kind: EntryKind::File, - metadata, - }) - } - - async fn write_file_stream(&self, path: &VirtualPath, mut stream: ByteStream, _size_hint: Option, options: &WriteOptions) -> CfkResult { - use futures::StreamExt; - - // Collect the stream into a single buffer, then delegate to write_file - 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 delete(&self, path: &VirtualPath, _options: &DeleteOptions) -> CfkResult<()> { - let smb_path = self.to_smb_posix_path(path); - - // Try rm first (file), then rmdir (directory) - let cmd = format!("rm \"{}\"", smb_path); - match self.run_smbclient_command(&cmd) { - Ok(_) => Ok(()), - Err(_) => { - // Might be a directory - let cmd = format!("rmdir \"{}\"", smb_path); - self.run_smbclient_command(&cmd)?; - Ok(()) - } - } - } - - async fn create_directory(&self, path: &VirtualPath) -> CfkResult { - let smb_path = self.to_smb_posix_path(path); - let cmd = format!("mkdir \"{}\"", smb_path); - self.run_smbclient_command(&cmd)?; - - Ok(Entry { - path: path.clone(), - kind: EntryKind::Directory, - metadata: Metadata::new(), - }) - } - - async fn copy(&self, from: &VirtualPath, to: &VirtualPath, _options: &CopyOptions) -> CfkResult { - if self.config.version == SmbVersion::Smb1 { - return Err(CfkError::Unsupported("SMB1 doesn't support server-side copy".into())); - } - - // smbclient does not have a native server-side copy command, - // so we download then re-upload. - let from_path = self.to_smb_posix_path(from); - let to_path = self.to_smb_posix_path(to); - - let tmp_dir = std::env::temp_dir(); - let tmp_file = tmp_dir.join(format!("cfk_smb_copy_{}", std::process::id())); - let tmp_path_str = tmp_file.display().to_string(); - - // Download source - let cmd = format!("get \"{}\" \"{}\"", from_path, tmp_path_str); - self.run_smbclient_command(&cmd)?; - - // Upload to destination - let cmd = format!("put \"{}\" \"{}\"", tmp_path_str, to_path); - let result = self.run_smbclient_command(&cmd); - let _ = std::fs::remove_file(&tmp_file); - result?; - - self.get_metadata(to).await - } - - async fn rename(&self, from: &VirtualPath, to: &VirtualPath, _options: &MoveOptions) -> CfkResult { - let from_path = self.to_smb_posix_path(from); - let to_path = self.to_smb_posix_path(to); - - let cmd = format!("rename \"{}\" \"{}\"", from_path, to_path); - self.run_smbclient_command(&cmd)?; - - self.get_metadata(to).await - } - - async fn get_space_info(&self) -> CfkResult { - // smbclient `du` command can provide space info in some cases - // but it's not reliable; return unknown. - Ok(SpaceInfo::unknown()) - } -} - -/// 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; - - /// Returns true if the directory attribute flag is set. - pub fn is_directory(&self) -> bool { - self.0 & Self::DIRECTORY != 0 - } - - /// Returns true if the hidden attribute flag is set. - pub fn is_hidden(&self) -> bool { - self.0 & Self::HIDDEN != 0 - } - - /// Returns true if the read-only attribute flag is set. - pub fn is_readonly(&self) -> bool { - self.0 & Self::READONLY != 0 - } - - /// Returns true if the reparse point (symlink) attribute flag is set. - pub fn is_symlink(&self) -> bool { - self.0 & Self::REPARSE_POINT != 0 - } -} - -/// SMB file information -#[derive(Debug, Clone, Default)] -pub struct SmbFileInfo { - /// Windows FILETIME for creation - pub creation_time: u64, - /// Windows FILETIME for last access - pub last_access_time: u64, - /// Windows FILETIME for last write - pub last_write_time: u64, - /// Windows FILETIME for change - pub change_time: u64, - /// File attribute flags - pub attributes: SmbFileAttributes, - /// Allocation size on disk - pub allocation_size: u64, - /// Logical end-of-file position (file size) - pub end_of_file: u64, - /// Unique file identifier - pub file_id: u64, -} - -impl SmbFileInfo { - /// Convert Windows FILETIME to Unix timestamp. - /// - /// FILETIME is 100-nanosecond intervals since Jan 1, 1601. - /// Unix epoch is Jan 1, 1970. - fn filetime_to_unix(ft: u64) -> Option { - const FILETIME_UNIX_DIFF: u64 = 116444736000000000; - if ft > FILETIME_UNIX_DIFF { - Some(((ft - FILETIME_UNIX_DIFF) / 10000000) as i64) - } else { - None - } - } - - /// Convert this SMB file info into a CFK Entry. - 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<()> { - 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::Network(format!("Failed to run mount: {}", e)))?; - - 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::Network(format!("Failed to run mount_smbfs: {}", e)))?; - - 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 971da21c..00000000 --- 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 a576747e..00000000 --- a/czech-file-knife/cfk-providers/src/transport.rs +++ /dev/null @@ -1,593 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Transport layer support -//! -//! Low-level transport protocols: -//! - TCP: Traditional reliable transport -//! - QUIC: Modern UDP-based transport (HTTP/3) — simplified UDP-based placeholder -//! - UDP: Unreliable datagram -//! - Unix sockets: Local IPC -//! - Named pipes: Windows IPC -//! - Multicast: UDP multicast send/recv via `join_multicast_v4` - -#![allow(dead_code)] // Some placeholder structs exist for future expansion - -use cfk_core::{CfkError, CfkResult}; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use tokio::net::TcpStream; -use tracing::{debug, warn}; - -/// Transport type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Transport { - /// Standard TCP - Tcp, - /// QUIC (UDP-based reliable transport) - Quic, - /// Raw UDP - Udp, - /// Unix domain socket - Unix, - /// Named pipe (Windows) - Pipe, - /// Pragmatic General Multicast (RFC 3208) - Pgm, - /// NACK-Oriented Reliable Multicast (RFC 5740) - Norm, - /// Reliable Multicast Transport Protocol - Rmtp, -} - -/// Connection configuration -#[derive(Debug, Clone)] -pub struct ConnectionConfig { - /// Transport protocol to use - pub transport: Transport, - /// Remote address (hostname or IP) - pub addr: String, - /// Remote port - pub port: u16, - /// Connection timeout in milliseconds - pub timeout_ms: u64, - /// Enable TCP keepalive - pub keepalive: bool, - /// Enable TCP_NODELAY - pub nodelay: bool, - /// Read/write buffer size - 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 { - /// Establish a new TCP connection to the configured address. - 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 }) - } - - /// Returns a reference to the underlying TCP stream. - pub fn inner(&self) -> &TcpStream { - &self.stream - } - - /// Consumes this wrapper and returns the underlying TCP stream. - pub fn into_inner(self) -> TcpStream { - self.stream - } -} - -/// QUIC configuration -#[derive(Debug, Clone)] -pub struct QuicConfig { - /// ALPN protocol identifiers for negotiation - pub alpn_protocols: Vec, - /// Idle timeout before connection close (milliseconds) - pub max_idle_timeout_ms: u64, - /// Keepalive ping interval (milliseconds) - pub keep_alive_interval_ms: Option, - /// Maximum concurrent bidirectional streams - pub max_concurrent_streams: u32, - /// Initial flow-control window size - 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 — simplified UDP-based placeholder -/// -/// Provides a basic reliable-over-UDP transport using sequence numbers -/// and acknowledgements. A production implementation should use the -/// `quinn` crate for full QUIC RFC 9000 compliance. -pub struct QuicConnection { - config: QuicConfig, - /// The underlying UDP socket used for the simplified transport - socket: std::net::UdpSocket, - /// Remote peer address - peer_addr: SocketAddr, - /// Next outgoing sequence number - next_seq: u64, -} - -impl QuicConnection { - /// Create a new simplified QUIC connection to the given address. - /// - /// This uses a basic UDP socket with application-level framing. - /// For production use, integrate the `quinn` crate instead. - pub async fn connect(addr: SocketAddr, _server_name: &str, config: QuicConfig) -> CfkResult { - debug!(addr = %addr, "Connecting via simplified QUIC transport"); - - let local_addr: SocketAddr = "0.0.0.0:0".parse().unwrap(); - let socket = std::net::UdpSocket::bind(local_addr) - .map_err(|e| CfkError::Network(format!("Failed to bind UDP socket: {}", e)))?; - - socket.connect(addr) - .map_err(|e| CfkError::Network(format!("Failed to connect UDP socket: {}", e)))?; - - // Set read timeout based on config idle timeout - let timeout = std::time::Duration::from_millis(config.max_idle_timeout_ms); - socket.set_read_timeout(Some(timeout)) - .map_err(|e| CfkError::Network(format!("Failed to set timeout: {}", e)))?; - - Ok(Self { - config, - socket, - peer_addr: addr, - next_seq: 0, - }) - } - - /// Open a new bidirectional stream. - /// - /// In this simplified implementation, each stream is a separate - /// framed message exchange over the single UDP socket. - pub async fn open_stream(&self) -> CfkResult { - Ok(QuicStream { - socket_fd: self.socket.try_clone() - .map_err(|e| CfkError::Network(format!("Failed to clone socket: {}", e)))?, - peer_addr: self.peer_addr, - }) - } - - /// Send raw data over the simplified QUIC transport. - /// - /// Frames the data with a 8-byte sequence number header. - pub fn send(&mut self, data: &[u8]) -> CfkResult { - // Simple framing: [8-byte seq LE] [payload] - let mut frame = Vec::with_capacity(8 + data.len()); - frame.extend_from_slice(&self.next_seq.to_le_bytes()); - frame.extend_from_slice(data); - self.next_seq += 1; - - self.socket.send(&frame) - .map_err(|e| CfkError::Network(format!("QUIC send failed: {}", e))) - } - - /// Receive raw data from the simplified QUIC transport. - /// - /// Returns the payload after stripping the sequence number header. - pub fn recv(&self, buf: &mut [u8]) -> CfkResult<(u64, usize)> { - let mut frame = vec![0u8; 8 + buf.len()]; - let n = self.socket.recv(&mut frame) - .map_err(|e| CfkError::Network(format!("QUIC recv failed: {}", e)))?; - - if n < 8 { - return Err(CfkError::Network("QUIC frame too short".into())); - } - - let seq = u64::from_le_bytes(frame[..8].try_into().unwrap()); - let payload_len = n - 8; - buf[..payload_len].copy_from_slice(&frame[8..n]); - - Ok((seq, payload_len)) - } -} - -/// QUIC bidirectional stream -/// -/// A lightweight wrapper around a cloned UDP socket that represents -/// one logical stream in the simplified QUIC transport. -pub struct QuicStream { - /// Cloned UDP socket for this stream - socket_fd: std::net::UdpSocket, - /// Remote peer address - peer_addr: SocketAddr, -} - -impl QuicStream { - /// Send data on this stream. - pub fn send(&self, data: &[u8]) -> CfkResult { - self.socket_fd.send(data) - .map_err(|e| CfkError::Network(format!("Stream send failed: {}", e))) - } - - /// Receive data from this stream. - pub fn recv(&self, buf: &mut [u8]) -> CfkResult { - self.socket_fd.recv(buf) - .map_err(|e| CfkError::Network(format!("Stream recv failed: {}", e))) - } -} - -/// Multi-transport connector -/// -/// Tries the preferred transport first, falling back to an optional -/// secondary transport on failure. -pub struct MultiTransport { - preferred: Transport, - fallback: Option, -} - -impl MultiTransport { - /// Create a new multi-transport connector with the given preferred protocol. - pub fn new(preferred: Transport) -> Self { - Self { preferred, fallback: None } - } - - /// Set a fallback transport to try if the preferred one fails. - 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 the simplified QUIC transport - let socket_addr: SocketAddr = format!("{}:{}", addr, port) - .parse() - .map_err(|e| CfkError::Network(format!("Invalid address: {}", e)))?; - let quic_config = QuicConfig::default(); - - match QuicConnection::connect(socket_addr, addr, quic_config).await { - Ok(conn) => Ok(Box::new(QuicTransportWrapper { _conn: conn })), - Err(e) => { - // Fall back to TCP if configured - if let Some(Transport::Tcp) = self.fallback { - warn!("QUIC connect failed ({}), falling back to TCP", e); - 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(e) - } - } - } - } - _ => Err(CfkError::Unsupported(format!("{:?} not implemented", self.preferred))), - } - } -} - -/// Abstract transport stream trait -pub trait TransportStream: Send + Sync { - /// Returns the transport protocol type for this stream. - fn transport_type(&self) -> Transport; -} - -impl TransportStream for TcpConnection { - fn transport_type(&self) -> Transport { - Transport::Tcp - } -} - -/// Wrapper to make QuicConnection implement TransportStream -struct QuicTransportWrapper { - _conn: QuicConnection, -} - -impl TransportStream for QuicTransportWrapper { - fn transport_type(&self) -> Transport { - Transport::Quic - } -} - -/// Reliable multicast support -/// -/// Provides basic UDP multicast send/receive using `std::net::UdpSocket` -/// with `join_multicast_v4`. PGM and NORM configurations set up the -/// underlying multicast group and rate parameters, but the transport -/// uses standard UDP multicast (not the full PGM/NORM wire protocols). -pub mod multicast { - use super::*; - - /// Multicast group configuration - #[derive(Debug, Clone)] - pub struct MulticastGroup { - /// Multicast group IPv4 address (must be in 224.0.0.0/4) - pub group_addr: Ipv4Addr, - /// UDP port for multicast traffic - pub port: u16, - /// Local network interface to bind (None = any) - pub interface: Option, - /// Time-to-live for multicast packets - pub ttl: u8, - /// Whether to receive own multicast packets - 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 { - /// Multicast group settings - pub group: MulticastGroup, - /// Rate limit in kilobits per second - pub rate_limit_kbps: u32, - /// Transmit window size - pub window_size: u32, - /// NAK repeat data interval in milliseconds - pub nak_rdata_ivl_ms: u32, - } - - 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 { - /// Multicast group settings - pub group: MulticastGroup, - /// Transmission rate in kilobits per second - pub rate_kbps: u32, - /// Buffer size in bytes - pub buffer_size: usize, - /// Maximum segment size - pub segment_size: u16, - /// Enable Forward Error Correction - pub fec_enabled: bool, - } - - impl Default for NormConfig { - fn default() -> Self { - Self { - group: MulticastGroup::default(), - rate_kbps: 10000, - buffer_size: 1048576, // 1MB - segment_size: 1400, - fec_enabled: true, - } - } - } - - /// Set up a UDP multicast socket from the given group configuration. - fn create_multicast_socket(group: &MulticastGroup) -> CfkResult { - let bind_addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, group.port); - let socket = std::net::UdpSocket::bind(bind_addr) - .map_err(|e| CfkError::Network(format!("Failed to bind multicast socket: {}", e)))?; - - let iface = group.interface.unwrap_or(Ipv4Addr::UNSPECIFIED); - - socket.join_multicast_v4(&group.group_addr, &iface) - .map_err(|e| CfkError::Network(format!("Failed to join multicast group: {}", e)))?; - - socket.set_multicast_ttl_v4(group.ttl as u32) - .map_err(|e| CfkError::Network(format!("Failed to set multicast TTL: {}", e)))?; - - socket.set_multicast_loop_v4(group.loopback) - .map_err(|e| CfkError::Network(format!("Failed to set multicast loopback: {}", e)))?; - - Ok(socket) - } - - /// Reliable multicast sender - /// - /// Uses standard UDP multicast to send data to all group members. - /// The PGM/NORM config parameters configure the multicast group; - /// actual wire-level reliability depends on the transport. - pub struct MulticastSender { - transport: Transport, - socket: std::net::UdpSocket, - dest_addr: SocketAddrV4, - } - - impl MulticastSender { - /// Create a PGM-style multicast sender. - /// - /// Uses UDP multicast as the underlying transport. The PGM - /// rate limit and window parameters are recorded but not - /// enforced at the wire level in this implementation. - pub async fn new_pgm(config: PgmConfig) -> CfkResult { - debug!( - group = %config.group.group_addr, - port = config.group.port, - "Creating PGM multicast sender" - ); - - let socket = create_multicast_socket(&config.group)?; - let dest_addr = SocketAddrV4::new(config.group.group_addr, config.group.port); - - Ok(Self { - transport: Transport::Pgm, - socket, - dest_addr, - }) - } - - /// Create a NORM-style multicast sender. - /// - /// Uses UDP multicast as the underlying transport. The NORM - /// FEC and rate parameters are recorded but not enforced at - /// the wire level in this implementation. - pub async fn new_norm(config: NormConfig) -> CfkResult { - debug!( - group = %config.group.group_addr, - port = config.group.port, - "Creating NORM multicast sender" - ); - - let socket = create_multicast_socket(&config.group)?; - let dest_addr = SocketAddrV4::new(config.group.group_addr, config.group.port); - - Ok(Self { - transport: Transport::Norm, - socket, - dest_addr, - }) - } - - /// Send data to all group members. - pub async fn send(&self, data: &[u8]) -> CfkResult<()> { - self.socket.send_to(data, self.dest_addr) - .map_err(|e| CfkError::Network(format!("Multicast send failed: {}", e)))?; - Ok(()) - } - - /// Send a file to all group members, chunked by the socket buffer size. - pub async fn send_file(&self, path: &std::path::Path) -> CfkResult<()> { - let data = std::fs::read(path) - .map_err(|e| CfkError::Other(format!("Failed to read file: {}", e)))?; - - // Send in chunks to avoid UDP fragmentation - const CHUNK_SIZE: usize = 1400; - for chunk in data.chunks(CHUNK_SIZE) { - self.send(chunk).await?; - } - - Ok(()) - } - } - - impl Drop for MulticastSender { - fn drop(&mut self) { - // Best-effort leave — ignore errors on cleanup - let _ = self.socket.leave_multicast_v4( - &self.dest_addr.ip().clone(), - &Ipv4Addr::UNSPECIFIED, - ); - } - } - - /// Reliable multicast receiver - /// - /// Joins a UDP multicast group and receives data from senders. - pub struct MulticastReceiver { - transport: Transport, - socket: std::net::UdpSocket, - group_addr: Ipv4Addr, - } - - impl MulticastReceiver { - /// Join a PGM-style multicast group as a receiver. - pub async fn join_pgm(config: PgmConfig) -> CfkResult { - debug!( - group = %config.group.group_addr, - port = config.group.port, - "Joining PGM multicast group" - ); - - let socket = create_multicast_socket(&config.group)?; - - Ok(Self { - transport: Transport::Pgm, - socket, - group_addr: config.group.group_addr, - }) - } - - /// Join a NORM-style multicast group as a receiver. - pub async fn join_norm(config: NormConfig) -> CfkResult { - debug!( - group = %config.group.group_addr, - port = config.group.port, - "Joining NORM multicast group" - ); - - let socket = create_multicast_socket(&config.group)?; - - Ok(Self { - transport: Transport::Norm, - socket, - group_addr: config.group.group_addr, - }) - } - - /// Receive data from the multicast group. - /// - /// Blocks until data is available or the socket times out. - pub async fn recv(&self) -> CfkResult> { - let mut buf = vec![0u8; 65536]; - let (n, _src) = self.socket.recv_from(&mut buf) - .map_err(|e| CfkError::Network(format!("Multicast recv failed: {}", e)))?; - buf.truncate(n); - Ok(buf) - } - } - - impl Drop for MulticastReceiver { - fn drop(&mut self) { - let _ = self.socket.leave_multicast_v4( - &self.group_addr, - &Ipv4Addr::UNSPECIFIED, - ); - } - } -} 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 48e9edc4..00000000 --- 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 7047c43c..00000000 --- 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 7c6c0606..00000000 --- a/czech-file-knife/cfk-search/src/lib.rs +++ /dev/null @@ -1,648 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Full-text search for Czech File Knife -//! -//! This module provides full-text search capabilities. -//! Includes a simple inverted-index implementation using -//! `HashMap>` for file content search, -//! as well as filename-based search and glob matching. - -#![forbid(unsafe_code)] -use async_trait::async_trait; -use cfk_core::{CfkError, CfkResult, Entry, VirtualPath}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use thiserror::Error; - -/// Search index errors -#[derive(Error, Debug)] -pub enum SearchError { - /// The requested index was not found - #[error("Index not found: {0}")] - IndexNotFound(String), - - /// An error occurred during indexing operations - #[error("Index error: {0}")] - IndexError(String), - - /// The search query could not be parsed - #[error("Query parse error: {0}")] - QueryError(String), - - /// An I/O error occurred - #[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 entry, optionally with its content bytes. - async fn index(&self, entry: &Entry, content: Option<&[u8]>) -> CfkResult<()>; - - /// Remove an entry from the index by its path. - async fn remove(&self, path: &VirtualPath) -> CfkResult<()>; - - /// Search the index and return matching results. - async fn search(&self, query: &SearchQuery) -> CfkResult>; - - /// Clear all entries from the index. - async fn clear(&self) -> CfkResult<()>; - - /// Get index statistics (document count, size, last update). - 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 (approximate) - pub size_bytes: u64, - /// Last update timestamp - pub last_updated: Option>, -} - -/// Internal document record stored in the inverted index -#[derive(Debug, Clone)] -struct IndexedDocument { - /// The CFK entry for this document - entry: Entry, - /// Tokenised words from the filename - name_tokens: Vec, - /// Tokenised words from the file content (if indexed) - content_tokens: Vec, -} - -/// Simple inverted-index search engine -/// -/// Uses a `HashMap>` mapping each unique lowercased -/// token to the list of document indices that contain it. Documents -/// are stored in a flat Vec alongside a parallel token map. -/// -/// This is suitable for small-to-medium corpora (thousands of files). -/// For larger workloads, enable the `tantivy` feature. -pub struct InvertedIndex { - /// All indexed documents, keyed by their VirtualPath URI - documents: Arc>>, - /// Inverted index: token -> list of document indices - token_index: Arc>>>, - /// Path to document-index mapping for fast removal - path_map: Arc>>, -} - -impl InvertedIndex { - /// Create a new empty inverted index. - pub fn new() -> Self { - Self { - documents: Arc::new(RwLock::new(Vec::new())), - token_index: Arc::new(RwLock::new(HashMap::new())), - path_map: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Tokenise a string into lowercase words suitable for indexing. - /// - /// Splits on whitespace and non-alphanumeric characters, filters - /// out very short tokens (< 2 chars), and lowercases everything. - fn tokenise(text: &str) -> Vec { - text.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') - .filter(|w| w.len() >= 2) - .map(|w| w.to_lowercase()) - .collect() - } - - /// Compute a simple TF-IDF-like score for a document against query tokens. - fn score_document(doc: &IndexedDocument, query_tokens: &[String], search_contents: bool) -> f32 { - if query_tokens.is_empty() { - return 0.0; - } - - let mut matched = 0u32; - let mut total_matches = 0u32; - - for qt in query_tokens { - // Name matches are weighted higher (x3) - let name_hits = doc.name_tokens.iter().filter(|t| t.contains(qt.as_str())).count() as u32; - if name_hits > 0 { - matched += 1; - total_matches += name_hits * 3; - } - - if search_contents { - let content_hits = doc.content_tokens.iter().filter(|t| t.contains(qt.as_str())).count() as u32; - if content_hits > 0 { - matched += 1; - total_matches += content_hits; - } - } - } - - if matched == 0 { - return 0.0; - } - - // Normalise: fraction of query tokens matched * log(total_matches) - let coverage = matched as f32 / (query_tokens.len() as f32 * 2.0); // /2 because name+content - let intensity = (1.0 + total_matches as f32).ln() / 10.0; - (coverage + intensity).min(1.0) - } - - /// Extract a snippet around the first occurrence of a query token. - fn extract_snippet(text: &str, query_token: &str, context_chars: usize) -> Option { - let lower = text.to_lowercase(); - if let Some(pos) = lower.find(query_token) { - let start = pos.saturating_sub(context_chars); - let end = (pos + query_token.len() + context_chars).min(text.len()); - // Find character boundaries - let start = text[..start].char_indices().last().map(|(i, _)| i).unwrap_or(0); - let end = text[end..].char_indices().next().map(|(i, _)| end + i).unwrap_or(text.len()); - let mut snippet = String::new(); - if start > 0 { - snippet.push_str("..."); - } - snippet.push_str(&text[start..end]); - if end < text.len() { - snippet.push_str("..."); - } - Some(snippet) - } else { - None - } - } -} - -impl Default for InvertedIndex { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl SearchIndex for InvertedIndex { - async fn index(&self, entry: &Entry, content: Option<&[u8]>) -> CfkResult<()> { - let path_key = entry.path.to_uri(); - - // Tokenise the filename - let name = entry.name().unwrap_or(""); - let name_tokens = Self::tokenise(name); - - // Tokenise the content (if provided and UTF-8) - let content_tokens = if let Some(bytes) = content { - if let Ok(text) = std::str::from_utf8(bytes) { - Self::tokenise(text) - } else { - Vec::new() - } - } else { - Vec::new() - }; - - let doc = IndexedDocument { - entry: entry.clone(), - name_tokens: name_tokens.clone(), - content_tokens: content_tokens.clone(), - }; - - // Insert into documents list - let mut docs = self.documents.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let mut path_map = self.path_map.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let mut token_idx = self.token_index.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - - // If already indexed, remove old entry first - if let Some(&old_idx) = path_map.get(&path_key) { - // Remove old token references (expensive but correct) - let old_doc = &docs[old_idx]; - for token in old_doc.name_tokens.iter().chain(old_doc.content_tokens.iter()) { - if let Some(indices) = token_idx.get_mut(token) { - indices.retain(|&i| i != old_idx); - } - } - docs[old_idx] = doc; - // Re-index tokens for this slot - let idx = old_idx; - for token in name_tokens.iter().chain(content_tokens.iter()) { - token_idx.entry(token.clone()).or_default().push(idx); - } - } else { - let idx = docs.len(); - docs.push(doc); - path_map.insert(path_key, idx); - - // Update inverted index - for token in name_tokens.iter().chain(content_tokens.iter()) { - token_idx.entry(token.clone()).or_default().push(idx); - } - } - - Ok(()) - } - - async fn remove(&self, path: &VirtualPath) -> CfkResult<()> { - let path_key = path.to_uri(); - - let mut path_map = self.path_map.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let docs = self.documents.read().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let mut token_idx = self.token_index.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - - if let Some(&idx) = path_map.get(&path_key) { - // Remove token references - let doc = &docs[idx]; - for token in doc.name_tokens.iter().chain(doc.content_tokens.iter()) { - if let Some(indices) = token_idx.get_mut(token) { - indices.retain(|&i| i != idx); - } - } - path_map.remove(&path_key); - } - - Ok(()) - } - - async fn search(&self, query: &SearchQuery) -> CfkResult> { - let query_tokens = Self::tokenise(&query.query); - if query_tokens.is_empty() { - return Ok(Vec::new()); - } - - let docs = self.documents.read().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let token_idx = self.token_index.read().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - - // Collect candidate document indices from the inverted index - let mut candidate_set = std::collections::HashSet::new(); - for qt in &query_tokens { - // Also match partial token prefixes - for (token, indices) in token_idx.iter() { - if token.contains(qt.as_str()) { - for &idx in indices { - candidate_set.insert(idx); - } - } - } - } - - // Score and filter candidates - let mut results: Vec = candidate_set.into_iter() - .filter_map(|idx| { - let doc = docs.get(idx)?; - - // Backend filter - if let Some(ref backends) = query.backends { - if !backends.contains(&doc.entry.path.backend) { - return None; - } - } - - // Path prefix filter - if let Some(ref paths) = query.paths { - let doc_path = doc.entry.path.to_path_string(); - if !paths.iter().any(|p| doc_path.starts_with(&p.to_path_string())) { - return None; - } - } - - // File type filter - if let Some(ref file_types) = query.file_types { - if let Some(ext) = doc.entry.path.extension() { - if !file_types.iter().any(|ft| ft.eq_ignore_ascii_case(ext)) { - return None; - } - } else { - return None; - } - } - - let score = Self::score_document(doc, &query_tokens, query.search_contents); - if score <= 0.0 { - return None; - } - - // Build snippets - let mut snippets = Vec::new(); - let name = doc.entry.name().unwrap_or(""); - for qt in &query_tokens { - if let Some(s) = Self::extract_snippet(name, qt, 20) { - snippets.push(s); - } - } - - Some(SearchResult { - entry: doc.entry.clone(), - score, - snippets, - }) - }) - .collect(); - - // Sort by score descending - results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); - - // Apply offset and limit - let offset = query.offset.unwrap_or(0); - let limit = query.limit.unwrap_or(100); - let results: Vec<_> = results.into_iter().skip(offset).take(limit).collect(); - - Ok(results) - } - - async fn clear(&self) -> CfkResult<()> { - let mut docs = self.documents.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let mut token_idx = self.token_index.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let mut path_map = self.path_map.write().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - - docs.clear(); - token_idx.clear(); - path_map.clear(); - - Ok(()) - } - - async fn stats(&self) -> CfkResult { - let docs = self.documents.read().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - let token_idx = self.token_index.read().map_err(|_| CfkError::Other("Lock poisoned".into()))?; - - // Approximate size: count of tokens * average key size + posting list sizes - let token_count: usize = token_idx.values().map(|v| v.len()).sum(); - let key_size: usize = token_idx.keys().map(|k| k.len()).sum(); - let approx_size = (key_size + token_count * 8) as u64; - - Ok(IndexStats { - document_count: docs.len() as u64, - size_bytes: approx_size, - last_updated: Some(chrono::Utc::now()), - }) - } -} - -/// Tantivy-based search index (stub) -/// Enable the `tantivy` feature to use this. -#[cfg(feature = "tantivy")] -pub struct TantivyIndex { - _path: std::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("Tantivy indexing not yet implemented".into())) - } - - async fn remove(&self, _path: &VirtualPath) -> CfkResult<()> { - Err(CfkError::Unsupported("Tantivy indexing not yet implemented".into())) - } - - async fn search(&self, _query: &SearchQuery) -> CfkResult> { - Err(CfkError::Unsupported("Tantivy search not yet implemented".into())) - } - - async fn clear(&self) -> CfkResult<()> { - Err(CfkError::Unsupported("Tantivy indexing not yet implemented".into())) - } - - async fn stats(&self) -> CfkResult { - Err(CfkError::Unsupported("Tantivy 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::*; - use cfk_core::{EntryKind, metadata::Metadata}; - - #[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")); - } - - fn make_entry(backend: &str, path: &str) -> Entry { - Entry { - path: VirtualPath::new(backend, path), - kind: EntryKind::File, - metadata: Metadata::new(), - } - } - - #[tokio::test] - async fn test_inverted_index_basic() { - let index = InvertedIndex::new(); - - // Index some entries - let entry1 = make_entry("local", "/docs/readme.txt"); - let entry2 = make_entry("local", "/docs/license.md"); - let entry3 = make_entry("local", "/src/main.rs"); - - index.index(&entry1, Some(b"This is the readme file with important documentation")).await.unwrap(); - index.index(&entry2, Some(b"MIT License - free software")).await.unwrap(); - index.index(&entry3, Some(b"fn main() { println!(\"hello\"); }")).await.unwrap(); - - // Search by filename - let query = SearchQuery { - query: "readme".to_string(), - search_contents: false, - ..Default::default() - }; - let results = index.search(&query).await.unwrap(); - assert!(!results.is_empty()); - assert!(results[0].entry.path.to_uri().contains("readme")); - - // Search by content - let query = SearchQuery { - query: "license".to_string(), - search_contents: true, - ..Default::default() - }; - let results = index.search(&query).await.unwrap(); - assert!(!results.is_empty()); - } - - #[tokio::test] - async fn test_inverted_index_stats() { - let index = InvertedIndex::new(); - - let entry = make_entry("local", "/test.txt"); - index.index(&entry, Some(b"hello world")).await.unwrap(); - - let stats = index.stats().await.unwrap(); - assert_eq!(stats.document_count, 1); - assert!(stats.size_bytes > 0); - } - - #[tokio::test] - async fn test_inverted_index_remove() { - let index = InvertedIndex::new(); - - let entry = make_entry("local", "/removeme.txt"); - index.index(&entry, Some(b"temporary content")).await.unwrap(); - - let query = SearchQuery { - query: "removeme".to_string(), - search_contents: false, - ..Default::default() - }; - let results = index.search(&query).await.unwrap(); - assert!(!results.is_empty()); - - // Remove and verify - index.remove(&entry.path).await.unwrap(); - let results = index.search(&query).await.unwrap(); - assert!(results.is_empty()); - } - - #[tokio::test] - async fn test_inverted_index_clear() { - let index = InvertedIndex::new(); - - let entry1 = make_entry("local", "/a.txt"); - let entry2 = make_entry("local", "/b.txt"); - index.index(&entry1, None).await.unwrap(); - index.index(&entry2, None).await.unwrap(); - - let stats = index.stats().await.unwrap(); - assert_eq!(stats.document_count, 2); - - index.clear().await.unwrap(); - - let stats = index.stats().await.unwrap(); - assert_eq!(stats.document_count, 0); - } - - #[tokio::test] - async fn test_inverted_index_file_type_filter() { - let index = InvertedIndex::new(); - - let txt = make_entry("local", "/notes.txt"); - let pdf = make_entry("local", "/report.pdf"); - index.index(&txt, Some(b"project notes")).await.unwrap(); - index.index(&pdf, Some(b"project report")).await.unwrap(); - - let query = SearchQuery { - query: "project".to_string(), - search_contents: true, - file_types: Some(vec!["txt".to_string()]), - ..Default::default() - }; - let results = index.search(&query).await.unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].entry.path.to_uri().contains("notes.txt")); - } - - #[tokio::test] - async fn test_inverted_index_pagination() { - let index = InvertedIndex::new(); - - for i in 0..10 { - let entry = make_entry("local", &format!("/file_{}.txt", i)); - index.index(&entry, Some(format!("common content {}", i).as_bytes())).await.unwrap(); - } - - let query = SearchQuery { - query: "file".to_string(), - search_contents: false, - limit: Some(3), - offset: Some(0), - ..Default::default() - }; - let results = index.search(&query).await.unwrap(); - assert_eq!(results.len(), 3); - - let query2 = SearchQuery { - query: "file".to_string(), - search_contents: false, - limit: Some(3), - offset: Some(3), - ..Default::default() - }; - let results2 = index.search(&query2).await.unwrap(); - assert_eq!(results2.len(), 3); - } -} 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 b1fb1ef3..00000000 --- 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 7b7b2ce5..00000000 --- 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 b3dc5fe4..00000000 --- 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 1f24a9de..00000000 --- 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 addc25de..00000000 --- a/czech-file-knife/cfk-vfs/src/lib.rs +++ /dev/null @@ -1,471 +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. -//! -//! Without the `fuse` feature, a passthrough VFS is provided that -//! delegates all operations to `std::fs` on a given root directory. -//! With the `fuse` feature, a FUSE filesystem using the `fuser` crate -//! is available. - -#![forbid(unsafe_code)] -use cfk_core::{CfkError, CfkResult}; -use std::path::{Path, PathBuf}; -use thiserror::Error; -use tracing::debug; - -/// VFS errors -#[derive(Error, Debug)] -pub enum VfsError { - /// The specified mount point directory does not exist - #[error("Mount point does not exist: {0}")] - MountPointNotFound(String), - - /// The specified mount point path is not a directory - #[error("Mount point is not a directory: {0}")] - MountPointNotDirectory(String), - - /// A filesystem is already mounted at the given path - #[error("Already mounted at: {0}")] - AlreadyMounted(String), - - /// No filesystem is currently mounted - #[error("Not mounted")] - NotMounted, - - /// An error from the FUSE layer - #[error("FUSE error: {0}")] - Fuse(String), - - /// An I/O error occurred - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Mount options controlling VFS behaviour -#[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 (extra logging) - pub debug: bool, -} - -/// VFS mount handle -/// -/// Without the `fuse` feature, this is a passthrough VFS that delegates -/// all operations to `std::fs` under the specified root directory. -/// With the `fuse` feature, this manages a FUSE mount. -pub struct VfsMount { - /// The local mount point or root directory - mount_point: PathBuf, - /// Mount options - _options: MountOptions, - /// Whether the mount is currently active - mounted: bool, -} - -impl VfsMount { - /// Mount a CFK backend at the given path. - /// - /// Without the `fuse` feature, this creates a passthrough VFS - /// rooted at `mount_point`. The `backend_id` is recorded for - /// identification but the passthrough delegates to local fs. - /// - /// # 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 { - let mount_point = mount_point.into(); - - // Validate mount point exists and is a directory - if !mount_point.exists() { - return Err(CfkError::Other(format!( - "Mount point does not exist: {}", mount_point.display() - ))); - } - - if !mount_point.is_dir() { - return Err(CfkError::Other(format!( - "Mount point is not a directory: {}", mount_point.display() - ))); - } - - debug!( - mount_point = %mount_point.display(), - "VFS passthrough mount created" - ); - - Ok(Self { - mount_point, - _options: options, - mounted: true, - }) - } - - /// 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 { - self.mounted && self.mount_point.exists() - } - - /// Unmount the filesystem. - pub fn unmount(mut self) -> CfkResult<()> { - self.mounted = false; - debug!( - mount_point = %self.mount_point.display(), - "VFS unmounted" - ); - Ok(()) - } - - // ---------------------------------------------------------------- - // Passthrough VFS operations (delegating to std::fs) - // ---------------------------------------------------------------- - - /// Read a file from the VFS. - pub fn read_file(&self, path: &Path) -> CfkResult> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - std::fs::read(&real).map_err(|e| CfkError::Other(format!( - "VFS read failed for {}: {}", real.display(), e - ))) - } - - /// Write a file to the VFS. - pub fn write_file(&self, path: &Path, data: &[u8]) -> CfkResult<()> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - - // Ensure parent directories exist - if let Some(parent) = real.parent() { - std::fs::create_dir_all(parent).map_err(|e| CfkError::Other(format!( - "VFS mkdir failed for {}: {}", parent.display(), e - )))?; - } - - std::fs::write(&real, data).map_err(|e| CfkError::Other(format!( - "VFS write failed for {}: {}", real.display(), e - ))) - } - - /// List entries in a directory. - pub fn read_dir(&self, path: &Path) -> CfkResult> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - let mut entries = Vec::new(); - - let read_dir = std::fs::read_dir(&real).map_err(|e| CfkError::Other(format!( - "VFS readdir failed for {}: {}", real.display(), e - )))?; - - for entry_result in read_dir { - let entry = entry_result.map_err(|e| CfkError::Other(format!( - "VFS readdir entry failed: {}", e - )))?; - - let name = entry.file_name().to_string_lossy().to_string(); - let file_type = entry.file_type().map_err(|e| CfkError::Other(format!( - "VFS file_type failed: {}", e - )))?; - - let kind = if file_type.is_dir() { - VfsFileKind::Directory - } else if file_type.is_symlink() { - VfsFileKind::Symlink - } else { - VfsFileKind::File - }; - - let metadata = entry.metadata().ok(); - let size = metadata.as_ref().map(|m| m.len()); - - entries.push(VfsDirEntry { name, kind, size }); - } - - Ok(entries) - } - - /// Get metadata for a path. - pub fn metadata(&self, path: &Path) -> CfkResult { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - let meta = std::fs::metadata(&real).map_err(|e| CfkError::Other(format!( - "VFS stat failed for {}: {}", real.display(), e - )))?; - - let kind = if meta.is_dir() { - VfsFileKind::Directory - } else if meta.is_symlink() { - VfsFileKind::Symlink - } else { - VfsFileKind::File - }; - - Ok(VfsMetadata { - kind, - size: meta.len(), - readonly: meta.permissions().readonly(), - }) - } - - /// Delete a file or directory. - pub fn delete(&self, path: &Path, recursive: bool) -> CfkResult<()> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - - if real.is_dir() { - if recursive { - std::fs::remove_dir_all(&real) - } else { - std::fs::remove_dir(&real) - } - } else { - std::fs::remove_file(&real) - } - .map_err(|e| CfkError::Other(format!( - "VFS delete failed for {}: {}", real.display(), e - ))) - } - - /// Create a directory (and any missing parents). - pub fn create_dir(&self, path: &Path) -> CfkResult<()> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real = self.resolve(path)?; - std::fs::create_dir_all(&real).map_err(|e| CfkError::Other(format!( - "VFS mkdir failed for {}: {}", real.display(), e - ))) - } - - /// Rename / move a file or directory. - pub fn rename(&self, from: &Path, to: &Path) -> CfkResult<()> { - if !self.mounted { - return Err(CfkError::Other("VFS not mounted".into())); - } - - let real_from = self.resolve(from)?; - let real_to = self.resolve(to)?; - - std::fs::rename(&real_from, &real_to).map_err(|e| CfkError::Other(format!( - "VFS rename failed: {} -> {}: {}", real_from.display(), real_to.display(), e - ))) - } - - // ---------------------------------------------------------------- - // Internal helpers - // ---------------------------------------------------------------- - - /// Resolve a relative or absolute path against the mount point root. - /// - /// Prevents path traversal attacks by ensuring the resolved path - /// stays within the mount point directory. - fn resolve(&self, path: &Path) -> CfkResult { - let mut resolved = self.mount_point.clone(); - - for component in path.components() { - match component { - std::path::Component::Normal(c) => resolved.push(c), - std::path::Component::RootDir => { /* skip leading / */ } - std::path::Component::ParentDir => { - // Prevent escaping the mount root - if resolved == self.mount_point { - return Err(CfkError::Other( - "Path traversal beyond mount root".into(), - )); - } - resolved.pop(); - } - std::path::Component::CurDir => { /* skip . */ } - _ => {} - } - } - - Ok(resolved) - } -} - -impl Drop for VfsMount { - fn drop(&mut self) { - if self.mounted { - debug!( - mount_point = %self.mount_point.display(), - "VFS dropped while mounted" - ); - self.mounted = false; - } - } -} - -/// File kind for VFS directory entries -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VfsFileKind { - /// Regular file - File, - /// Directory - Directory, - /// Symbolic link - Symlink, -} - -/// A directory entry returned by `VfsMount::read_dir` -#[derive(Debug, Clone)] -pub struct VfsDirEntry { - /// Entry name (filename only, not full path) - pub name: String, - /// File kind - pub kind: VfsFileKind, - /// File size in bytes (None for directories/symlinks on some platforms) - pub size: Option, -} - -/// Metadata for a VFS entry -#[derive(Debug, Clone)] -pub struct VfsMetadata { - /// File kind - pub kind: VfsFileKind, - /// Size in bytes - pub size: u64, - /// Whether the file is read-only - pub readonly: bool, -} - -/// List active mounts (currently always empty for passthrough VFS) -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_nonexistent_fails() { - let result = VfsMount::mount("local", "/tmp/nonexistent_cfk_test_dir_12345", MountOptions::default()); - assert!(result.is_err()); - } - - #[test] - fn test_passthrough_lifecycle() { - // Create a temp dir as our "mount point" - let tmp = std::env::temp_dir().join(format!("cfk_vfs_test_{}", std::process::id())); - std::fs::create_dir_all(&tmp).unwrap(); - - // Mount - let vfs = VfsMount::mount("local", &tmp, MountOptions::default()).unwrap(); - assert!(vfs.is_mounted()); - assert_eq!(vfs.mount_point(), &tmp); - - // Write a file - vfs.write_file(Path::new("hello.txt"), b"Hello, VFS!").unwrap(); - - // Read it back - let data = vfs.read_file(Path::new("hello.txt")).unwrap(); - assert_eq!(data, b"Hello, VFS!"); - - // List directory - let entries = vfs.read_dir(Path::new("")).unwrap(); - assert!(entries.iter().any(|e| e.name == "hello.txt")); - - // Get metadata - let meta = vfs.metadata(Path::new("hello.txt")).unwrap(); - assert_eq!(meta.kind, VfsFileKind::File); - assert_eq!(meta.size, 11); - - // Create subdirectory - vfs.create_dir(Path::new("subdir")).unwrap(); - let meta = vfs.metadata(Path::new("subdir")).unwrap(); - assert_eq!(meta.kind, VfsFileKind::Directory); - - // Rename - vfs.rename(Path::new("hello.txt"), Path::new("renamed.txt")).unwrap(); - let entries = vfs.read_dir(Path::new("")).unwrap(); - assert!(entries.iter().any(|e| e.name == "renamed.txt")); - assert!(!entries.iter().any(|e| e.name == "hello.txt")); - - // Delete - vfs.delete(Path::new("renamed.txt"), false).unwrap(); - vfs.delete(Path::new("subdir"), true).unwrap(); - - // Unmount - vfs.unmount().unwrap(); - - // Cleanup - let _ = std::fs::remove_dir_all(&tmp); - } - - #[test] - fn test_path_traversal_prevention() { - let tmp = std::env::temp_dir().join(format!("cfk_vfs_traversal_{}", std::process::id())); - std::fs::create_dir_all(&tmp).unwrap(); - - let vfs = VfsMount::mount("local", &tmp, MountOptions::default()).unwrap(); - - // Attempting to read ../../etc/passwd should fail - let result = vfs.read_file(Path::new("../../etc/passwd")); - assert!(result.is_err()); - - vfs.unmount().unwrap(); - let _ = std::fs::remove_dir_all(&tmp); - } -} diff --git a/czech-file-knife/contractiles/README.adoc b/czech-file-knife/contractiles/README.adoc deleted file mode 100644 index d19a3877..00000000 --- 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 314903cc..00000000 --- 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/k9/README.adoc b/czech-file-knife/contractiles/k9/README.adoc deleted file mode 100644 index 16107f14..00000000 --- a/czech-file-knife/contractiles/k9/README.adoc +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -= K9 Contractiles -:toc: left -:icons: font - -== What Are K9 Contractiles? - -K9 contractiles are self-validating components that combine configuration, validation, and deployment logic in a single file format. They implement the RSR principle of "self-describing artifacts" by embedding contracts and orchestration directly in the component. - -== The Three Security Levels - -K9 components declare their trust requirements using "The Leash" security model: - -[horizontal] -`'Kennel`:: Pure data, no execution (safest) -`'Yard`:: Nickel evaluation with contracts (medium trust) -`'Hunt`:: Full execution with Just recipes (requires signature) - -== Example Components - -This directory contains example K9 contractiles for common repository tasks: - -=== Kennel Level (Pure Data) - -**File:** `examples/project-metadata.k9.ncl` - -Pure configuration data with no execution. Safe to include in any repository. - -**Use cases:** -- Project metadata (name, version, description) -- Build configuration -- Tool settings -- Data schemas - -**Security:** No signature required, data-only. - -=== Yard Level (Validated Config) - -**File:** `examples/ci-config.k9.ncl` - -Configuration with Nickel contracts for runtime validation. Evaluated safely without I/O. - -**Use cases:** -- CI/CD configuration with validation -- Deployment parameters -- Database schemas with constraints -- API specifications - -**Security:** Signature recommended, Nickel evaluation only. - -=== Hunt Level (Full Execution) - -**File:** `examples/setup-repo.k9.ncl` - -Full execution with Just recipes. Can run shell commands and modify filesystem. - -**Use cases:** -- Repository setup scripts -- Deployment automation -- System configuration -- Package installation - -**Security:** **Signature required**, full system access. - -== Usage in Your Repository - -=== 1. Create K9 Components - -Choose the appropriate security level for your use case: - -[source,bash] ----- -# Kennel: Pure configuration -cp contractiles/k9/examples/project-metadata.k9.ncl config/metadata.k9.ncl - -# Yard: Validated configuration -cp contractiles/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl - -# Hunt: Full automation -cp contractiles/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl ----- - -=== 2. Validate Components - -[source,bash] ----- -# Validate Nickel syntax and contracts -nickel typecheck config/metadata.k9.ncl - -# Verify Hunt-level signature (if signed) -./must verify scripts/setup.k9.ncl ----- - -=== 3. Execute Components - -[source,bash] ----- -# Kennel: Export as JSON -nickel export config/metadata.k9.ncl > metadata.json - -# Yard: Evaluate with validation -nickel eval .github/ci.k9.ncl - -# Hunt: Run with Just (dry-run first!) -./must --dry-run run scripts/setup.k9.ncl -./must run scripts/setup.k9.ncl ----- - -== Integration with RSR - -K9 contractiles integrate with other RSR standards: - -**STATE.scm**:: K9 components can generate or validate STATE.scm -**ECOSYSTEM.scm**:: K9 can automate cross-repo operations -**META.scm**:: K9 can enforce architectural decisions - -== Security Best Practices - -=== For Kennel/Yard Components - -✅ **Safe to use without signatures** + -✅ **Review Nickel code before use** + -✅ **Validate contracts match expectations** - -=== For Hunt Components - -⚠️ **ALWAYS verify signatures** + -⚠️ **Review Just recipes carefully** + -⚠️ **Run dry-run mode first** + -⚠️ **Never run as root unless required** + -⚠️ **Sandbox external components** - -**See:** https://github.com/hyperpolymath/standards/blob/main/k9-svc/docs/SECURITY-BEST-PRACTICES.adoc - -== Template Files - -Use these as starting points for your own K9 components: - -- `template-kennel.k9.ncl` - Pure data template -- `template-yard.k9.ncl` - Validated config template -- `template-hunt.k9.ncl` - Full execution template - -== Dependencies - -To use K9 contractiles in your repository: - -[source,bash] ----- -# Install Nickel (configuration language) -curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-linux-x86_64 -o nickel -chmod +x nickel && sudo mv nickel /usr/local/bin/ - -# Install Just (task runner, for Hunt level) -cargo install just - -# Clone K9-SVC (for must shim and tooling) -git clone https://github.com/hyperpolymath/standards.git -# Note: K9-SVC is located in standards/k9-svc ----- - -== Learn More - -- **K9-SVC Specification:** https://github.com/hyperpolymath/standards/blob/main/k9-svc/SPEC.adoc -- **K9 User Guide:** https://github.com/hyperpolymath/standards/blob/main/k9-svc/GUIDE.adoc -- **Security Documentation:** https://github.com/hyperpolymath/standards/blob/main/k9-svc/docs/SECURITY-FAQ.adoc -- **IANA Media Type:** `application/vnd.k9+nickel` - -== Contributing - -When adding K9 contractiles to your repository: - -1. Use appropriate security level (Kennel > Yard > Hunt) -2. Document what each component does -3. Include validation contracts in Yard/Hunt components -4. Sign Hunt-level components before committing -5. Add K9 validation to CI/CD pipeline - -**Questions?** Open an issue on https://github.com/hyperpolymath/standards/tree/main/k9-svc diff --git a/czech-file-knife/contractiles/k9/examples/ci-config.k9.ncl b/czech-file-knife/contractiles/k9/examples/ci-config.k9.ncl deleted file mode 100644 index 9fe314e2..00000000 --- a/czech-file-knife/contractiles/k9/examples/ci-config.k9.ncl +++ /dev/null @@ -1,126 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# Example Yard-level K9 component: CI/CD configuration with validation -# Security Level: Yard (Nickel evaluation, contract validation) -# Signature recommended but not required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "ci-configuration", - security = { - leash = 'Yard, - trust_level = "validated-config", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "ci-config", - version = "1.0.0", - description = "CI/CD configuration with runtime validation", - author = "Jonathan D.A. Jewell ", - }, - }, - - # CI/CD configuration with Nickel contracts - ci = { - # Platform must be a known CI provider - platform - | [| 'GitHubActions, 'GitLabCI, 'CircleCI, 'TravisCI |] - = 'GitHubActions, - - # Build matrix with validation - matrix = { - # Operating systems to test on - os - | Array String - | std.array.NonEmpty - = ["ubuntu-latest", "macos-latest"], - - # Language versions to test - versions - | Array String - | std.array.NonEmpty - = ["stable", "beta"], - }, - - # Workflow steps with validation - steps = [ - { - name = "Checkout", - action = "actions/checkout@v4", - # Version must be SHA-pinned for security - sha | String | std.string.NonEmpty = "b4ffde65f46336ab88eb53be808477a3936bae11", - }, - { - name = "Build", - run = "just build", - }, - { - name = "Test", - run = "just test", - }, - { - name = "Lint", - run = "just lint", - }, - ], - - # Deployment configuration - deploy = { - enabled | Bool = false, - - # Only deploy from main branch - branch - | String - | std.contract.from_predicate (fun b => b == "main" || b == "master") - = "main", - - # Deployment requires manual approval - requires_approval | Bool = true, - }, - - # Security scanning - security = { - enabled | Bool = true, - - scanners = [ - { - name = "CodeQL", - languages = ["rust", "javascript"], - }, - { - name = "OSSF Scorecard", - enabled = true, - }, - { - name = "TruffleHog", - scan_for = "secrets", - }, - ], - }, - - # Notification settings - notifications = { - on_success = "never", - on_failure = "always", - channels = ["email"], - }, - }, - - # Validation rules (enforced by Nickel) - validation = { - # At least one OS must be specified - check_os = std.array.length ci.matrix.os > 0, - - # At least one version must be tested - check_versions = std.array.length ci.matrix.versions > 0, - - # Must have at least build and test steps - check_steps = std.array.length ci.steps >= 2, - - # Security scanning must be enabled - check_security = ci.security.enabled == true, - }, -} diff --git a/czech-file-knife/contractiles/k9/examples/project-metadata.k9.ncl b/czech-file-knife/contractiles/k9/examples/project-metadata.k9.ncl deleted file mode 100644 index b2299b44..00000000 --- a/czech-file-knife/contractiles/k9/examples/project-metadata.k9.ncl +++ /dev/null @@ -1,57 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# Example Kennel-level K9 component: Project metadata -# Security Level: Kennel (pure data, no execution) -# No signature required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "project-metadata", - security = { - leash = 'Kennel, - trust_level = "data-only", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "project-metadata", - version = "1.0.0", - description = "Pure data configuration for project metadata", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Project configuration - project = { - name = "my-project", - version = "0.1.0", - description = "A project following Rhodium Standard Repositories", - - repository = { - url = "https://github.com/hyperpolymath/my-project", - type = "git", - }, - - author = { - name = "Jonathan D.A. Jewell", - email = "j.d.a.jewell@open.ac.uk", - organization = "The Open University", - }, - - license = "MPL-2.0", - - keywords = [ - "rhodium-standard", - "rsr", - "hyperpolymath", - ], - }, - - # Export as JSON for other tools - export = { - format = "json", - destination = "project-metadata.json", - }, -} diff --git a/czech-file-knife/contractiles/k9/examples/setup-repo.k9.ncl b/czech-file-knife/contractiles/k9/examples/setup-repo.k9.ncl deleted file mode 100644 index b635d5bf..00000000 --- a/czech-file-knife/contractiles/k9/examples/setup-repo.k9.ncl +++ /dev/null @@ -1,167 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# Example Hunt-level K9 component: Repository setup automation -# Security Level: Hunt (full execution with Just recipes) -# ⚠️ SIGNATURE REQUIRED - DO NOT RUN WITHOUT VERIFICATION - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "repository-setup", - security = { - leash = 'Hunt, - trust_level = "full-system-access", - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - signature_required = true, - }, - metadata = { - name = "setup-repo", - version = "1.0.0", - description = "Automated repository setup with RSR standards", - author = "Jonathan D.A. Jewell ", - }, - warnings = [ - "This component has full system access", - "Only run from trusted sources with verified signatures", - "Review Just recipes before execution", - "Use dry-run mode first: ./must --dry-run run setup-repo.k9.ncl", - ], - }, - - # Configuration with contracts - config = { - repo_name - | String - | std.string.NonEmpty - = "my-new-repo", - - repo_type - | [| 'Library, 'Application, 'Tool, 'Specification |] - = 'Application, - - primary_language - | String - | std.string.NonEmpty - = "rust", - - # RSR compliance features to enable - features = { - checkpoint_files | Bool = true, # STATE.scm, ECOSYSTEM.scm, META.scm - security_workflows | Bool = true, # CodeQL, Scorecard, etc. - quality_checks | Bool = true, # Linting, formatting - mirroring | Bool = false, # GitLab/Bitbucket mirrors - }, - - # Git configuration - git = { - default_branch = "main", - initial_commit | Bool = true, - remote_url | String = "", - }, - }, - - # Just recipes for execution - # These run when: ./must run setup-repo.k9.ncl - recipes = { - # Main entry point - default = { - recipe = "setup", - description = "Set up RSR-compliant repository", - }, - - # Individual setup tasks - setup = { - dependencies = ["check-env", "create-structure", "init-git", "setup-workflows"], - commands = [ - "echo '✅ Repository setup complete!'", - "echo 'Run: git status to see changes'", - ], - }, - - "check-env" = { - description = "Verify required tools are installed", - commands = [ - "command -v git || (echo 'ERROR: git not found' && exit 1)", - "command -v just || (echo 'ERROR: just not found' && exit 1)", - "command -v nickel || (echo 'ERROR: nickel not found' && exit 1)", - "echo '✓ All required tools present'", - ], - }, - - "create-structure" = { - description = "Create RSR directory structure", - commands = [ - "mkdir -p src/ docs/ tests/ scripts/", - "mkdir -p .github/workflows/", - "mkdir -p contractiles/k9/", - "echo '✓ Directory structure created'", - ], - }, - - "init-git" = { - description = "Initialize Git repository", - commands = [ - "git init -b %{config.git.default_branch}", - "git config user.name 'Jonathan D.A. Jewell'", - "git config user.email 'j.d.a.jewell@open.ac.uk'", - "echo '✓ Git initialized'", - ], - }, - - "setup-workflows" = { - description = "Add RSR-compliant workflows", - commands = [ - # This would copy workflow templates - # In a real implementation, would fetch from rsr-template-repo - "echo '✓ Workflows configured'", - ], - }, - - "create-checkpoint-files" = { - description = "Create STATE.scm, ECOSYSTEM.scm, META.scm", - commands = [ - "echo '(state (version \"1.0.0\") (project \"%{config.repo_name}\"))' > STATE.scm", - "echo '(ecosystem (version \"1.0.0\") (name \"%{config.repo_name}\"))' > ECOSYSTEM.scm", - "echo '(meta (version \"1.0.0\") (project \"%{config.repo_name}\"))' > META.scm", - "echo '✓ Checkpoint files created'", - ], - }, - - "add-license" = { - description = "Add PMPL-1.0 license", - commands = [ - "curl -sL https://raw.githubusercontent.com/hyperpolymath/pmpl/main/LICENSE -o LICENSE", - "echo '✓ License added'", - ], - }, - - "add-readme" = { - description = "Create README.adoc from template", - commands = [ - "echo '= %{config.repo_name}' > README.adoc", - "echo '' >> README.adoc", - "echo 'Part of the Hyperpolymath ecosystem.' >> README.adoc", - "echo '✓ README created'", - ], - }, - - clean = { - description = "Remove generated files (careful!)", - commands = [ - "echo '⚠️ This will delete all generated files'", - "echo 'Press Ctrl+C to cancel, or wait 5 seconds...'", - "sleep 5", - "rm -f STATE.scm ECOSYSTEM.scm META.scm", - "echo '✓ Cleaned'", - ], - }, - }, - - # Validation (Yard-level checks before Hunt execution) - validation = { - check_repo_name = std.string.length config.repo_name > 0, - check_language = std.string.length config.primary_language > 0, - }, -} diff --git a/czech-file-knife/contractiles/k9/template-hunt.k9.ncl b/czech-file-knife/contractiles/k9/template-hunt.k9.ncl deleted file mode 100644 index b3fcb475..00000000 --- a/czech-file-knife/contractiles/k9/template-hunt.k9.ncl +++ /dev/null @@ -1,136 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# K9 Hunt-level template: Full execution with Just recipes -# Security Level: Hunt (full system access) -# ⚠️ SIGNATURE REQUIRED - Review carefully before use - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'deployment', 'setup-script')", - security = { - leash = 'Hunt, - trust_level = "full-system-access", - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - signature_required = true, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Detailed description of what this component does", - author = "Jonathan D.A. Jewell ", - }, - warnings = [ - "This component has full system access", - "Only run from trusted sources with verified signatures", - "Review all Just recipes before execution", - "Use dry-run mode first: ./must --dry-run run your-file.k9.ncl", - ], - side_effects = [ - "TODO: List what files/directories this creates or modifies", - "TODO: List what commands this executes", - "TODO: List what network access this requires", - ], - }, - - # Configuration with contracts (Yard-level validation) - config = { - # Add your configuration here with appropriate contracts - target_dir - | String - | std.string.NonEmpty - = "/tmp/k9-output", - - dry_run | Bool = false, - - # Add more config as needed - }, - - # Just recipes for execution - # These run when: ./must run your-file.k9.ncl - recipes = { - # Main entry point (runs by default) - default = { - recipe = "TODO: main-task", - description = "TODO: What the default recipe does", - }, - - # Define your recipes here - "main-task" = { - dependencies = ["check-prerequisites"], - commands = [ - "echo 'TODO: Add your commands here'", - # Example: Create directory - # "mkdir -p %{config.target_dir}", - # Example: Run a command - # "just build", - # Example: Conditional execution - # "@if [ \"%{config.dry_run}\" = \"true\" ]; then echo '[DRY-RUN] Would execute'; else actual-command; fi", - ], - }, - - "check-prerequisites" = { - description = "Verify required tools and permissions", - commands = [ - # Example: Check for required tools - # "command -v git || (echo 'ERROR: git not found' && exit 1)", - # Example: Check permissions - # "[ -w %{config.target_dir} ] || (echo 'ERROR: Cannot write to target directory' && exit 1)", - "echo '✓ Prerequisites checked'", - ], - }, - - # Add more recipes as needed - "build" = { - description = "Build the project", - commands = [ - "echo 'TODO: Add build commands'", - ], - }, - - "deploy" = { - description = "Deploy the application", - dependencies = ["build"], - commands = [ - "echo 'TODO: Add deployment commands'", - ], - }, - - "clean" = { - description = "Clean up generated files", - commands = [ - "echo '⚠️ This will delete files - waiting 3 seconds...'", - "sleep 3", - "echo 'TODO: Add cleanup commands'", - # "rm -rf %{config.target_dir}", - ], - }, - }, - - # Validation (Yard-level checks before Hunt execution) - validation = { - check_target_dir = std.string.length config.target_dir > 0, - # Add more validation as needed - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Define configuration with contracts -# 3. Implement Just recipes with your commands -# 4. Test with dry-run: ./must --dry-run run your-file.k9.ncl -# 5. Review dry-run output carefully -# 6. Sign the component: ./must sign your-file.k9.ncl -# 7. Distribute with signature: your-file.k9.ncl.sig -# 8. Users verify and run: ./must verify && ./must run your-file.k9.ncl -# -# Security checklist: -# ✓ All TODO items filled in -# ✓ side_effects documented accurately -# ✓ Commands reviewed for safety -# ✓ No hardcoded secrets or credentials -# ✓ Proper error handling in recipes -# ✓ Tested in dry-run mode -# ✓ Component signed with trusted key diff --git a/czech-file-knife/contractiles/k9/template-kennel.k9.ncl b/czech-file-knife/contractiles/k9/template-kennel.k9.ncl deleted file mode 100644 index 4228b26c..00000000 --- a/czech-file-knife/contractiles/k9/template-kennel.k9.ncl +++ /dev/null @@ -1,54 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# K9 Kennel-level template: Pure data configuration -# Security Level: Kennel (data-only, no execution) -# No signature required - safe for any use - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'build-config', 'metadata')", - security = { - leash = 'Kennel, - trust_level = "data-only", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Brief description of what this component contains", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Your configuration data here - config = { - # Example: Pure data values - setting_1 = "value", - setting_2 = 42, - setting_3 = true, - - nested = { - key = "value", - }, - - list = [ - "item1", - "item2", - ], - }, - - # Optional: Export format specification - export = { - format = "json", # or "yaml", "toml" - destination = "output.json", - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Add your configuration data to config = { ... } -# 3. Validate: nickel typecheck your-file.k9.ncl -# 4. Export: nickel export your-file.k9.ncl > output.json diff --git a/czech-file-knife/contractiles/k9/template-yard.k9.ncl b/czech-file-knife/contractiles/k9/template-yard.k9.ncl deleted file mode 100644 index a723f5af..00000000 --- a/czech-file-knife/contractiles/k9/template-yard.k9.ncl +++ /dev/null @@ -1,84 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# K9 Yard-level template: Configuration with validation -# Security Level: Yard (Nickel evaluation with contracts) -# Signature recommended but not required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'validated-config', 'schema')", - security = { - leash = 'Yard, - trust_level = "validated-config", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Brief description with validation details", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Configuration with Nickel contracts for validation - config = { - # Example: String that cannot be empty - name - | String - | std.string.NonEmpty - = "TODO: default value", - - # Example: Number with range constraint - port - | Number - | std.contract.from_predicate (fun p => p > 0 && p < 65536) - = 8080, - - # Example: Boolean flag - enabled | Bool = true, - - # Example: Enum (one of several values) - environment - | [| 'Development, 'Staging, 'Production |] - = 'Development, - - # Example: List with non-empty constraint - items - | Array String - | std.array.NonEmpty - = ["item1", "item2"], - - # Example: Nested object with contracts - database = { - host | String | std.string.NonEmpty = "localhost", - port | Number | std.contract.from_predicate (fun p => p > 0 && p < 65536) = 5432, - name | String | std.string.NonEmpty = "mydb", - }, - }, - - # Validation rules (additional cross-field checks) - validation = { - # Example: Check that at least one item exists - check_items = std.array.length config.items > 0, - - # Example: Check that production has secure settings - check_production = - if config.environment == 'Production then - config.enabled == true - else - true, - - # Add your custom validation rules here - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Define your config with appropriate contracts -# 3. Add validation rules in validation = { ... } -# 4. Validate: nickel typecheck your-file.k9.ncl -# 5. Evaluate: nickel eval your-file.k9.ncl -# 6. If validation passes, use in your application diff --git a/czech-file-knife/contractiles/must/Mustfile b/czech-file-knife/contractiles/must/Mustfile deleted file mode 100644 index dc7b3be5..00000000 --- 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 90f29229..00000000 --- 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 9905364f..00000000 --- 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 = {MPL-2.0} -} ----- - -== 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 9954ff73..00000000 --- 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 3c8031aa..00000000 --- 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/eclexiaiser.toml b/czech-file-knife/eclexiaiser.toml deleted file mode 100644 index 58d996b6..00000000 --- a/czech-file-knife/eclexiaiser.toml +++ /dev/null @@ -1,26 +0,0 @@ -# eclexiaiser manifest — energy/carbon resource budgets -# SPDX-License-Identifier: MPL-2.0 - -[project] -name = "czech-file-knife" - -[[functions]] -name = "cp" -source = "cfk-cli/src/commands.rs" -energy-budget-mj = 40.0 -carbon-budget-mg = 8.0 - -[[functions]] -name = "ls" -source = "cfk-cli/src/commands.rs" -energy-budget-mj = 10.0 -carbon-budget-mg = 2.0 - -[carbon] -provider = "static" -region = "GB" -static-intensity = 200.0 - -[report] -format = "text" -include-recommendations = true 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 ee775a44..00000000 --- a/czech-file-knife/examples/web-project-deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "// NOTE": "Example deno.json for AffineScript web projects", - "tasks": { - "build": "deno run -A npm:affinescript", - "clean": "deno run -A npm:affinescript clean", - "watch": "deno run -A npm:affinescript -w", - "serve": "deno run -A jsr:@std/http/file-server .", - "test": "deno test --allow-all" - }, - "imports": { - "affinescript": "^12.0.0", - "@affinescript/core": "npm:@affinescript/core@^1.6.0", - "safe-dom/": "https://raw.githubusercontent.com/hyperpolymath/affinescript-dom-mounter/main/src/", - "proven/": "../proven/bindings/affinescript/src/" - }, - "compilerOptions": { - "allowJs": true, - "checkJs": false - } -} diff --git a/czech-file-knife/ffi/zig/build.zig b/czech-file-knife/ffi/zig/build.zig deleted file mode 100644 index 424973e1..00000000 --- a/czech-file-knife/ffi/zig/build.zig +++ /dev/null @@ -1,94 +0,0 @@ -// CZECH_FILE_KNIFE FFI Build Configuration -// SPDX-License-Identifier: MPL-2.0 - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Shared library (.so, .dylib, .dll) - const lib = b.addSharedLibrary(.{ - .name = "czech-file-knife", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Set version - lib.version = .{ .major = 0, .minor = 1, .patch = 0 }; - - // Static library (.a) - const lib_static = b.addStaticLibrary(.{ - .name = "czech-file-knife", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Install artifacts - b.installArtifact(lib); - b.installArtifact(lib_static); - - // Generate header file for C compatibility - const header = b.addInstallHeader( - b.path("include/czech-file-knife.h"), - "czech-file-knife.h", - ); - b.getInstallStep().dependOn(&header.step); - - // Unit tests - const lib_tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - const run_lib_tests = b.addRunArtifact(lib_tests); - - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&run_lib_tests.step); - - // Integration tests - const integration_tests = b.addTest(.{ - .root_source_file = b.path("test/integration_test.zig"), - .target = target, - .optimize = optimize, - }); - - integration_tests.linkLibrary(lib); - - const run_integration_tests = b.addRunArtifact(integration_tests); - - const integration_test_step = b.step("test-integration", "Run integration tests"); - integration_test_step.dependOn(&run_integration_tests.step); - - // Documentation - const docs = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = .Debug, - }); - - const docs_step = b.step("docs", "Generate documentation"); - docs_step.dependOn(&b.addInstallDirectory(.{ - .source_dir = docs.getEmittedDocs(), - .install_dir = .prefix, - .install_subdir = "docs", - }).step); - - // Benchmark (if needed) - const bench = b.addExecutable(.{ - .name = "czech-file-knife-bench", - .root_source_file = b.path("bench/bench.zig"), - .target = target, - .optimize = .ReleaseFast, - }); - - bench.linkLibrary(lib); - - const run_bench = b.addRunArtifact(bench); - - const bench_step = b.step("bench", "Run benchmarks"); - bench_step.dependOn(&run_bench.step); -} diff --git a/czech-file-knife/ffi/zig/src/main.zig b/czech-file-knife/ffi/zig/src/main.zig deleted file mode 100644 index 73903e1f..00000000 --- a/czech-file-knife/ffi/zig/src/main.zig +++ /dev/null @@ -1,274 +0,0 @@ -// CZECH_FILE_KNIFE FFI Implementation -// -// This module implements the C-compatible FFI declared in src/abi/Foreign.idr -// All types and layouts must match the Idris2 ABI definitions. -// -// SPDX-License-Identifier: MPL-2.0 - -const std = @import("std"); - -// Version information (keep in sync with project) -const VERSION = "0.1.0"; -const BUILD_INFO = "CZECH_FILE_KNIFE built with Zig " ++ @import("builtin").zig_version_string; - -/// Thread-local error storage -threadlocal var last_error: ?[]const u8 = null; - -/// Set the last error message -fn setError(msg: []const u8) void { - last_error = msg; -} - -/// Clear the last error -fn clearError() void { - last_error = null; -} - -//============================================================================== -// Core Types (must match src/abi/Types.idr) -//============================================================================== - -/// Result codes (must match Idris2 Result type) -pub const Result = enum(c_int) { - ok = 0, - @"error" = 1, - invalid_param = 2, - out_of_memory = 3, - null_pointer = 4, -}; - -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { - // Internal state hidden from C - allocator: std.mem.Allocator, - initialized: bool, - // Add your fields here -}; - -//============================================================================== -// Library Lifecycle -//============================================================================== - -/// Initialize the library -/// Returns a handle, or null on failure -export fn czech-file-knife_init() ?*Handle { - const allocator = std.heap.c_allocator; - - const handle = allocator.create(Handle) catch { - setError("Failed to allocate handle"); - return null; - }; - - // Initialize handle - handle.* = .{ - .allocator = allocator, - .initialized = true, - }; - - clearError(); - return handle; -} - -/// Free the library handle -export fn czech-file-knife_free(handle: ?*Handle) void { - const h = handle orelse return; - const allocator = h.allocator; - - // Clean up resources - h.initialized = false; - - allocator.destroy(h); - clearError(); -} - -//============================================================================== -// Core Operations -//============================================================================== - -/// Process data (example operation) -export fn czech-file-knife_process(handle: ?*Handle, input: u32) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Example processing logic - _ = input; - - clearError(); - return .ok; -} - -//============================================================================== -// String Operations -//============================================================================== - -/// Get a string result (example) -/// Caller must free the returned string -export fn czech-file-knife_get_string(handle: ?*Handle) ?[*:0]const u8 { - const h = handle orelse { - setError("Null handle"); - return null; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return null; - } - - // Example: allocate and return a string - const result = h.allocator.dupeZ(u8, "Example result") catch { - setError("Failed to allocate string"); - return null; - }; - - clearError(); - return result.ptr; -} - -/// Free a string allocated by the library -export fn czech-file-knife_free_string(str: ?[*:0]const u8) void { - const s = str orelse return; - const allocator = std.heap.c_allocator; - - const slice = std.mem.span(s); - allocator.free(slice); -} - -//============================================================================== -// Array/Buffer Operations -//============================================================================== - -/// Process an array of data -export fn czech-file-knife_process_array( - handle: ?*Handle, - buffer: ?[*]const u8, - len: u32, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const buf = buffer orelse { - setError("Null buffer"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Access the buffer - const data = buf[0..len]; - _ = data; - - // Process data here - - clearError(); - return .ok; -} - -//============================================================================== -// Error Handling -//============================================================================== - -/// Get the last error message -/// Returns null if no error -export fn czech-file-knife_last_error() ?[*:0]const u8 { - const err = last_error orelse return null; - - // Return C string (static storage, no need to free) - const allocator = std.heap.c_allocator; - const c_str = allocator.dupeZ(u8, err) catch return null; - return c_str.ptr; -} - -//============================================================================== -// Version Information -//============================================================================== - -/// Get the library version -export fn czech-file-knife_version() [*:0]const u8 { - return VERSION.ptr; -} - -/// Get build information -export fn czech-file-knife_build_info() [*:0]const u8 { - return BUILD_INFO.ptr; -} - -//============================================================================== -// Callback Support -//============================================================================== - -/// Callback function type (C ABI) -pub const Callback = *const fn (u64, u32) callconv(.C) u32; - -/// Register a callback -export fn czech-file-knife_register_callback( - handle: ?*Handle, - callback: ?Callback, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const cb = callback orelse { - setError("Null callback"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Store callback for later use - _ = cb; - - clearError(); - return .ok; -} - -//============================================================================== -// Utility Functions -//============================================================================== - -/// Check if handle is initialized -export fn czech-file-knife_is_initialized(handle: ?*Handle) u32 { - const h = handle orelse return 0; - return if (h.initialized) 1 else 0; -} - -//============================================================================== -// Tests -//============================================================================== - -test "lifecycle" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - try std.testing.expect(czech-file-knife_is_initialized(handle) == 1); -} - -test "error handling" { - const result = czech-file-knife_process(null, 0); - try std.testing.expectEqual(Result.null_pointer, result); - - const err = czech-file-knife_last_error(); - try std.testing.expect(err != null); -} - -test "version" { - const ver = czech-file-knife_version(); - const ver_str = std.mem.span(ver); - try std.testing.expectEqualStrings(VERSION, ver_str); -} diff --git a/czech-file-knife/ffi/zig/test/integration_test.zig b/czech-file-knife/ffi/zig/test/integration_test.zig deleted file mode 100644 index 2945ab67..00000000 --- a/czech-file-knife/ffi/zig/test/integration_test.zig +++ /dev/null @@ -1,182 +0,0 @@ -// CZECH_FILE_KNIFE Integration Tests -// SPDX-License-Identifier: MPL-2.0 -// -// These tests verify that the Zig FFI correctly implements the Idris2 ABI - -const std = @import("std"); -const testing = std.testing; - -// Import FFI functions -extern fn czech-file-knife_init() ?*opaque {}; -extern fn czech-file-knife_free(?*opaque {}) void; -extern fn czech-file-knife_process(?*opaque {}, u32) c_int; -extern fn czech-file-knife_get_string(?*opaque {}) ?[*:0]const u8; -extern fn czech-file-knife_free_string(?[*:0]const u8) void; -extern fn czech-file-knife_last_error() ?[*:0]const u8; -extern fn czech-file-knife_version() [*:0]const u8; -extern fn czech-file-knife_is_initialized(?*opaque {}) u32; - -//============================================================================== -// Lifecycle Tests -//============================================================================== - -test "create and destroy handle" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - try testing.expect(handle != null); -} - -test "handle is initialized" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - const initialized = czech-file-knife_is_initialized(handle); - try testing.expectEqual(@as(u32, 1), initialized); -} - -test "null handle is not initialized" { - const initialized = czech-file-knife_is_initialized(null); - try testing.expectEqual(@as(u32, 0), initialized); -} - -//============================================================================== -// Operation Tests -//============================================================================== - -test "process with valid handle" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - const result = czech-file-knife_process(handle, 42); - try testing.expectEqual(@as(c_int, 0), result); // 0 = ok -} - -test "process with null handle returns error" { - const result = czech-file-knife_process(null, 42); - try testing.expectEqual(@as(c_int, 4), result); // 4 = null_pointer -} - -//============================================================================== -// String Tests -//============================================================================== - -test "get string result" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - const str = czech-file-knife_get_string(handle); - defer if (str) |s| czech-file-knife_free_string(s); - - try testing.expect(str != null); -} - -test "get string with null handle" { - const str = czech-file-knife_get_string(null); - try testing.expect(str == null); -} - -//============================================================================== -// Error Handling Tests -//============================================================================== - -test "last error after null handle operation" { - _ = czech-file-knife_process(null, 0); - - const err = czech-file-knife_last_error(); - try testing.expect(err != null); - - if (err) |e| { - const err_str = std.mem.span(e); - try testing.expect(err_str.len > 0); - } -} - -test "no error after successful operation" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - _ = czech-file-knife_process(handle, 0); - - // Error should be cleared after successful operation - // (This depends on implementation) -} - -//============================================================================== -// Version Tests -//============================================================================== - -test "version string is not empty" { - const ver = czech-file-knife_version(); - const ver_str = std.mem.span(ver); - - try testing.expect(ver_str.len > 0); -} - -test "version string is semantic version format" { - const ver = czech-file-knife_version(); - const ver_str = std.mem.span(ver); - - // Should be in format X.Y.Z - try testing.expect(std.mem.count(u8, ver_str, ".") >= 1); -} - -//============================================================================== -// Memory Safety Tests -//============================================================================== - -test "multiple handles are independent" { - const h1 = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(h1); - - const h2 = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(h2); - - try testing.expect(h1 != h2); - - // Operations on h1 should not affect h2 - _ = czech-file-knife_process(h1, 1); - _ = czech-file-knife_process(h2, 2); -} - -test "double free is safe" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - - czech-file-knife_free(handle); - czech-file-knife_free(handle); // Should not crash -} - -test "free null is safe" { - czech-file-knife_free(null); // Should not crash -} - -//============================================================================== -// Thread Safety Tests (if applicable) -//============================================================================== - -test "concurrent operations" { - const handle = czech-file-knife_init() orelse return error.InitFailed; - defer czech-file-knife_free(handle); - - const ThreadContext = struct { - h: *opaque {}, - id: u32, - }; - - const thread_fn = struct { - fn run(ctx: ThreadContext) void { - _ = czech-file-knife_process(ctx.h, ctx.id); - } - }.run; - - var threads: [4]std.Thread = undefined; - for (&threads, 0..) |*thread, i| { - thread.* = try std.Thread.spawn(.{}, thread_fn, .{ - ThreadContext{ .h = handle, .id = @intCast(i) }, - }); - } - - for (threads) |thread| { - thread.join(); - } -} diff --git a/czech-file-knife/fuzz/Cargo.toml b/czech-file-knife/fuzz/Cargo.toml deleted file mode 100644 index 32d5d63e..00000000 --- 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 = ["Jonathan D.A. Jewell "] -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 4da762fd..00000000 --- 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 f9142f8a..00000000 --- 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 15b52c3d..00000000 --- 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 1999b018..00000000 --- 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 697092b5..00000000 --- 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 cc81cf18..00000000 --- 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 711e372c..00000000 --- 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 92a8961e..00000000 --- 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 c3221df1..00000000 --- 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 95c9556b..00000000 --- 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 af5dab0f..00000000 --- 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 ddbbbf2c..00000000 --- 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=('MPL-2.0') -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 558bc450..00000000 --- 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 d7ea2709..00000000 --- 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: MPL-2.0 diff --git a/czech-file-knife/packaging/debian/rules b/czech-file-knife/packaging/debian/rules deleted file mode 100644 index 9010ce11..00000000 --- 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 87afca3f..00000000 --- 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 20820668..00000000 --- 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 MPL-2.0 -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 1a3ab9ec..00000000 --- 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: MPL-2.0 -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 093df8dc..00000000 --- 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": "MPL-2.0", - "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 ce809c22..00000000 --- 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: MPL-2.0 -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 diff --git a/czech-file-knife/src/abi/Foreign.idr b/czech-file-knife/src/abi/Foreign.idr deleted file mode 100644 index 47ca0b2f..00000000 --- a/czech-file-knife/src/abi/Foreign.idr +++ /dev/null @@ -1,216 +0,0 @@ -||| Foreign Function Interface Declarations -||| -||| This module declares all C-compatible functions that will be -||| implemented in the Zig FFI layer. -||| -||| All functions are declared here with type signatures and safety proofs. -||| Implementations live in ffi/zig/ - -module CZECH_FILE_KNIFE.ABI.Foreign - -import CZECH_FILE_KNIFE.ABI.Types -import CZECH_FILE_KNIFE.ABI.Layout - -%default total - --------------------------------------------------------------------------------- --- Library Lifecycle --------------------------------------------------------------------------------- - -||| Initialize the library -||| Returns a handle to the library instance, or Nothing on failure -export -%foreign "C:czech-file-knife_init, libczech-file-knife" -prim__init : PrimIO Bits64 - -||| Safe wrapper for library initialization -export -init : IO (Maybe Handle) -init = do - ptr <- primIO prim__init - pure (createHandle ptr) - -||| Clean up library resources -export -%foreign "C:czech-file-knife_free, libczech-file-knife" -prim__free : Bits64 -> PrimIO () - -||| Safe wrapper for cleanup -export -free : Handle -> IO () -free h = primIO (prim__free (handlePtr h)) - --------------------------------------------------------------------------------- --- Core Operations --------------------------------------------------------------------------------- - -||| Example operation: process data -export -%foreign "C:czech-file-knife_process, libczech-file-knife" -prim__process : Bits64 -> Bits32 -> PrimIO Bits32 - -||| Safe wrapper with error handling -export -process : Handle -> Bits32 -> IO (Either Result Bits32) -process h input = do - result <- primIO (prim__process (handlePtr h) input) - pure $ case result of - 0 => Left Error - n => Right n - --------------------------------------------------------------------------------- --- String Operations --------------------------------------------------------------------------------- - -||| Convert C string to Idris String -export -%foreign "support:idris2_getString, libidris2_support" -prim__getString : Bits64 -> String - -||| Free C string -export -%foreign "C:czech-file-knife_free_string, libczech-file-knife" -prim__freeString : Bits64 -> PrimIO () - -||| Get string result from library -export -%foreign "C:czech-file-knife_get_string, libczech-file-knife" -prim__getResult : Bits64 -> PrimIO Bits64 - -||| Safe string getter -export -getString : Handle -> IO (Maybe String) -getString h = do - ptr <- primIO (prim__getResult (handlePtr h)) - if ptr == 0 - then pure Nothing - else do - let str = prim__getString ptr - primIO (prim__freeString ptr) - pure (Just str) - --------------------------------------------------------------------------------- --- Array/Buffer Operations --------------------------------------------------------------------------------- - -||| Process array data -export -%foreign "C:czech-file-knife_process_array, libczech-file-knife" -prim__processArray : Bits64 -> Bits64 -> Bits32 -> PrimIO Bits32 - -||| Safe array processor -export -processArray : Handle -> (buffer : Bits64) -> (len : Bits32) -> IO (Either Result ()) -processArray h buf len = do - result <- primIO (prim__processArray (handlePtr h) buf len) - pure $ case resultFromInt result of - Just Ok => Right () - Just err => Left err - Nothing => Left Error - where - resultFromInt : Bits32 -> Maybe Result - resultFromInt 0 = Just Ok - resultFromInt 1 = Just Error - resultFromInt 2 = Just InvalidParam - resultFromInt 3 = Just OutOfMemory - resultFromInt 4 = Just NullPointer - resultFromInt _ = Nothing - --------------------------------------------------------------------------------- --- Error Handling --------------------------------------------------------------------------------- - -||| Get last error message -export -%foreign "C:czech-file-knife_last_error, libczech-file-knife" -prim__lastError : PrimIO Bits64 - -||| Retrieve last error as string -export -lastError : IO (Maybe String) -lastError = do - ptr <- primIO prim__lastError - if ptr == 0 - then pure Nothing - else pure (Just (prim__getString ptr)) - -||| Get error description for result code -export -errorDescription : Result -> String -errorDescription Ok = "Success" -errorDescription Error = "Generic error" -errorDescription InvalidParam = "Invalid parameter" -errorDescription OutOfMemory = "Out of memory" -errorDescription NullPointer = "Null pointer" - --------------------------------------------------------------------------------- --- Version Information --------------------------------------------------------------------------------- - -||| Get library version -export -%foreign "C:czech-file-knife_version, libczech-file-knife" -prim__version : PrimIO Bits64 - -||| Get version as string -export -version : IO String -version = do - ptr <- primIO prim__version - pure (prim__getString ptr) - -||| Get library build info -export -%foreign "C:czech-file-knife_build_info, libczech-file-knife" -prim__buildInfo : PrimIO Bits64 - -||| Get build information -export -buildInfo : IO String -buildInfo = do - ptr <- primIO prim__buildInfo - pure (prim__getString ptr) - --------------------------------------------------------------------------------- --- Callback Support --------------------------------------------------------------------------------- - -||| Callback function type (C ABI) -public export -Callback : Type -Callback = Bits64 -> Bits32 -> Bits32 - -||| Register a callback -export -%foreign "C:czech-file-knife_register_callback, libczech-file-knife" -prim__registerCallback : Bits64 -> AnyPtr -> PrimIO Bits32 - -||| Safe callback registration -export -registerCallback : Handle -> Callback -> IO (Either Result ()) -registerCallback h cb = do - result <- primIO (prim__registerCallback (handlePtr h) (cast cb)) - pure $ case resultFromInt result of - Just Ok => Right () - Just err => Left err - Nothing => Left Error - where - resultFromInt : Bits32 -> Maybe Result - resultFromInt 0 = Just Ok - resultFromInt _ = Just Error - --------------------------------------------------------------------------------- --- Utility Functions --------------------------------------------------------------------------------- - -||| Check if library is initialized -export -%foreign "C:czech-file-knife_is_initialized, libczech-file-knife" -prim__isInitialized : Bits64 -> PrimIO Bits32 - -||| Check initialization status -export -isInitialized : Handle -> IO Bool -isInitialized h = do - result <- primIO (prim__isInitialized (handlePtr h)) - pure (result /= 0) diff --git a/czech-file-knife/src/abi/Layout.idr b/czech-file-knife/src/abi/Layout.idr deleted file mode 100644 index 2498dd33..00000000 --- a/czech-file-knife/src/abi/Layout.idr +++ /dev/null @@ -1,176 +0,0 @@ -||| Memory Layout Proofs -||| -||| This module provides formal proofs about memory layout, alignment, -||| and padding for C-compatible structs. -||| -||| @see https://en.wikipedia.org/wiki/Data_structure_alignment - -module CZECH_FILE_KNIFE.ABI.Layout - -import CZECH_FILE_KNIFE.ABI.Types -import Data.Vect -import Data.So - -%default total - --------------------------------------------------------------------------------- --- Alignment Utilities --------------------------------------------------------------------------------- - -||| Calculate padding needed for alignment -public export -paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat -paddingFor offset alignment = - if offset `mod` alignment == 0 - then 0 - else alignment - (offset `mod` alignment) - -||| Proof that alignment divides aligned size -public export -data Divides : Nat -> Nat -> Type where - DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m - -||| Round up to next alignment boundary -public export -alignUp : (size : Nat) -> (alignment : Nat) -> Nat -alignUp size alignment = - size + paddingFor size alignment - -||| Proof that alignUp produces aligned result -public export -alignUpCorrect : (size : Nat) -> (align : Nat) -> (align > 0) -> Divides align (alignUp size align) -alignUpCorrect size align prf = - -- Proof that (size + padding) is divisible by align - DivideBy ((size + paddingFor size align) `div` align) Refl - --------------------------------------------------------------------------------- --- Struct Field Layout --------------------------------------------------------------------------------- - -||| A field in a struct with its offset and size -public export -record Field where - constructor MkField - name : String - offset : Nat - size : Nat - alignment : Nat - -||| Calculate the offset of the next field -public export -nextFieldOffset : Field -> Nat -nextFieldOffset f = alignUp (f.offset + f.size) f.alignment - -||| A struct layout is a list of fields with proofs -public export -record StructLayout where - constructor MkStructLayout - fields : Vect n Field - totalSize : Nat - alignment : Nat - {auto 0 sizeCorrect : So (totalSize >= sum (map (\f => f.size) fields))} - {auto 0 aligned : Divides alignment totalSize} - -||| Calculate total struct size with padding -public export -calcStructSize : Vect n Field -> Nat -> Nat -calcStructSize [] align = 0 -calcStructSize (f :: fs) align = - let lastOffset = foldl (\acc, field => nextFieldOffset field) f.offset fs - lastSize = foldr (\field, _ => field.size) f.size fs - in alignUp (lastOffset + lastSize) align - -||| Proof that field offsets are correctly aligned -public export -data FieldsAligned : Vect n Field -> Type where - NoFields : FieldsAligned [] - ConsField : - (f : Field) -> - (rest : Vect n Field) -> - Divides f.alignment f.offset -> - FieldsAligned rest -> - FieldsAligned (f :: rest) - -||| Verify a struct layout is valid -public export -verifyLayout : (fields : Vect n Field) -> (align : Nat) -> Either String StructLayout -verifyLayout fields align = - let size = calcStructSize fields align - in case decSo (size >= sum (map (\f => f.size) fields)) of - Yes prf => Right (MkStructLayout fields size align) - No _ => Left "Invalid struct size" - --------------------------------------------------------------------------------- --- Platform-Specific Layouts --------------------------------------------------------------------------------- - -||| Struct layout may differ by platform -public export -PlatformLayout : Platform -> Type -> Type -PlatformLayout p t = StructLayout - -||| Verify layout is correct for all platforms -public export -verifyAllPlatforms : - (layouts : (p : Platform) -> PlatformLayout p t) -> - Either String () -verifyAllPlatforms layouts = - -- Check that layout is valid on all platforms - Right () - --------------------------------------------------------------------------------- --- C ABI Compatibility --------------------------------------------------------------------------------- - -||| Proof that a struct follows C ABI rules -public export -data CABICompliant : StructLayout -> Type where - CABIOk : - (layout : StructLayout) -> - FieldsAligned layout.fields -> - CABICompliant layout - -||| Check if layout follows C ABI -public export -checkCABI : (layout : StructLayout) -> Either String (CABICompliant layout) -checkCABI layout = - -- Verify C ABI rules - Right (CABIOk layout ?fieldsAlignedProof) - --------------------------------------------------------------------------------- --- Example Layouts --------------------------------------------------------------------------------- - -||| Example: Simple struct layout -public export -exampleLayout : StructLayout -exampleLayout = - MkStructLayout - [ MkField "x" 0 4 4 -- Bits32 at offset 0 - , MkField "y" 8 8 8 -- Bits64 at offset 8 (4 bytes padding) - , MkField "z" 16 8 8 -- Double at offset 16 - ] - 24 -- Total size: 24 bytes - 8 -- Alignment: 8 bytes - -||| Proof that example layout is valid -export -exampleLayoutValid : CABICompliant exampleLayout -exampleLayoutValid = CABIOk exampleLayout ?exampleFieldsAligned - --------------------------------------------------------------------------------- --- Offset Calculation --------------------------------------------------------------------------------- - -||| Calculate field offset with proof of correctness -public export -fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (n : Nat ** Field) -fieldOffset layout name = - case findIndex (\f => f.name == name) layout.fields of - Just idx => Just (finToNat idx ** index idx layout.fields) - Nothing => Nothing - -||| Proof that field offset is within struct bounds -public export -offsetInBounds : (layout : StructLayout) -> (f : Field) -> So (f.offset + f.size <= layout.totalSize) -offsetInBounds layout f = ?offsetInBoundsProof diff --git a/czech-file-knife/src/abi/Types.idr b/czech-file-knife/src/abi/Types.idr deleted file mode 100644 index 96c75581..00000000 --- a/czech-file-knife/src/abi/Types.idr +++ /dev/null @@ -1,231 +0,0 @@ -||| ABI Type Definitions Template -||| -||| This module defines the Application Binary Interface (ABI) for this library. -||| All type definitions include formal proofs of correctness. -||| -||| Replace CZECH_FILE_KNIFE with your project name. -||| Replace {{TYPES}} with your actual type definitions. -||| -||| @see https://idris2.readthedocs.io for Idris2 documentation - -module CZECH_FILE_KNIFE.ABI.Types - -import Data.Bits -import Data.So -import Data.Vect - -%default total - --------------------------------------------------------------------------------- --- Platform Detection --------------------------------------------------------------------------------- - -||| Supported platforms for this ABI -public export -data Platform = Linux | Windows | MacOS | BSD | WASM - -||| Compile-time platform detection -||| This will be set during compilation based on target -public export -thisPlatform : Platform -thisPlatform = - %runElab do - -- Platform detection logic - pure Linux -- Default, override with compiler flags - --------------------------------------------------------------------------------- --- Core Types --------------------------------------------------------------------------------- - -||| Result codes for FFI operations -||| Use C-compatible integers for cross-language compatibility -public export -data Result : Type where - ||| Operation succeeded - Ok : Result - ||| Generic error - Error : Result - ||| Invalid parameter provided - InvalidParam : Result - ||| Out of memory - OutOfMemory : Result - ||| Null pointer encountered - NullPointer : Result - -||| Convert Result to C integer -public export -resultToInt : Result -> Bits32 -resultToInt Ok = 0 -resultToInt Error = 1 -resultToInt InvalidParam = 2 -resultToInt OutOfMemory = 3 -resultToInt NullPointer = 4 - -||| Results are decidably equal -public export -DecEq Result where - decEq Ok Ok = Yes Refl - decEq Error Error = Yes Refl - decEq InvalidParam InvalidParam = Yes Refl - decEq OutOfMemory OutOfMemory = Yes Refl - decEq NullPointer NullPointer = Yes Refl - decEq _ _ = No absurd - --------------------------------------------------------------------------------- --- Opaque Handles --------------------------------------------------------------------------------- - -||| Opaque handle type for FFI -||| Prevents direct construction, enforces creation through safe API -public export -data Handle : Type where - MkHandle : (ptr : Bits64) -> {auto 0 nonNull : So (ptr /= 0)} -> Handle - -||| Safely create a handle from a pointer value -||| Returns Nothing if pointer is null -public export -createHandle : Bits64 -> Maybe Handle -createHandle 0 = Nothing -createHandle ptr = Just (MkHandle ptr) - -||| Extract pointer value from handle -public export -handlePtr : Handle -> Bits64 -handlePtr (MkHandle ptr) = ptr - --------------------------------------------------------------------------------- --- Platform-Specific Types --------------------------------------------------------------------------------- - -||| C int size varies by platform -public export -CInt : Platform -> Type -CInt Linux = Bits32 -CInt Windows = Bits32 -CInt MacOS = Bits32 -CInt BSD = Bits32 -CInt WASM = Bits32 - -||| C size_t varies by platform -public export -CSize : Platform -> Type -CSize Linux = Bits64 -CSize Windows = Bits64 -CSize MacOS = Bits64 -CSize BSD = Bits64 -CSize WASM = Bits32 - -||| C pointer size varies by platform -public export -ptrSize : Platform -> Nat -ptrSize Linux = 64 -ptrSize Windows = 64 -ptrSize MacOS = 64 -ptrSize BSD = 64 -ptrSize WASM = 32 - -||| Pointer type for platform -public export -CPtr : Platform -> Type -> Type -CPtr p _ = Bits (ptrSize p) - --------------------------------------------------------------------------------- --- Memory Layout Proofs --------------------------------------------------------------------------------- - -||| Proof that a type has a specific size -public export -data HasSize : Type -> Nat -> Type where - SizeProof : {0 t : Type} -> {n : Nat} -> HasSize t n - -||| Proof that a type has a specific alignment -public export -data HasAlignment : Type -> Nat -> Type where - AlignProof : {0 t : Type} -> {n : Nat} -> HasAlignment t n - -||| Size of C types (platform-specific) -public export -cSizeOf : (p : Platform) -> (t : Type) -> Nat -cSizeOf p (CInt _) = 4 -cSizeOf p (CSize _) = if ptrSize p == 64 then 8 else 4 -cSizeOf p Bits32 = 4 -cSizeOf p Bits64 = 8 -cSizeOf p Double = 8 -cSizeOf p _ = ptrSize p `div` 8 - -||| Alignment of C types (platform-specific) -public export -cAlignOf : (p : Platform) -> (t : Type) -> Nat -cAlignOf p (CInt _) = 4 -cAlignOf p (CSize _) = if ptrSize p == 64 then 8 else 4 -cAlignOf p Bits32 = 4 -cAlignOf p Bits64 = 8 -cAlignOf p Double = 8 -cAlignOf p _ = ptrSize p `div` 8 - --------------------------------------------------------------------------------- --- Example Struct with Layout Proof --------------------------------------------------------------------------------- - -||| Example C-compatible struct -||| Replace this with your actual data types -public export -record ExampleStruct where - constructor MkExampleStruct - field1 : Bits32 - field2 : Bits64 - field3 : Double - -||| Prove the struct has correct size -public export -exampleStructSize : (p : Platform) -> HasSize ExampleStruct 16 -exampleStructSize p = - -- 4 bytes (Bits32) + 4 padding + 8 bytes (Bits64) + 8 bytes (Double) = 24 - -- But with alignment, it's actually platform-specific - SizeProof - -||| Prove the struct has correct alignment -public export -exampleStructAlign : (p : Platform) -> HasAlignment ExampleStruct 8 -exampleStructAlign p = AlignProof - --------------------------------------------------------------------------------- --- FFI Declarations --------------------------------------------------------------------------------- - -||| Declare external C functions -||| These will be implemented in Zig FFI -namespace Foreign - - ||| External function example - export - %foreign "C:example_function, libexample" - prim__exampleFunction : Bits64 -> PrimIO Bits32 - - ||| Safe wrapper around FFI function - export - exampleFunction : Handle -> IO (Either Result Bits32) - exampleFunction h = do - result <- primIO (prim__exampleFunction (handlePtr h)) - pure (Right result) - --------------------------------------------------------------------------------- --- Verification --------------------------------------------------------------------------------- - -||| Compile-time verification of ABI properties -namespace Verify - - ||| Verify struct sizes are correct - export - verifySizes : IO () - verifySizes = do - -- Add compile-time checks here - putStrLn "ABI sizes verified" - - ||| Verify struct alignments are correct - export - verifyAlignments : IO () - verifyAlignments = do - -- Add compile-time checks here - putStrLn "ABI alignments verified" diff --git a/llm-warmup-dev.adoc b/llm-warmup-dev.adoc index 55b6235a..7fc5c86d 100644 --- a/llm-warmup-dev.adoc +++ b/llm-warmup-dev.adoc @@ -27,7 +27,6 @@ ambientops/ Hybrid monorepo ├── displace/ (Rust) Displacement tool ├── personal-sysadmin/ (Rust) Personal sysadmin toolkit ├── panoptes/ (Rust) Monitoring -├── czech-file-knife/ (Rust) File manipulation tool ├── volumod/ (V) Volume modifier ├── emergency-button/ (V) Emergency button ├── network-dashboard/ (Elixir) Network monitoring @@ -80,9 +79,8 @@ just sync-metadata # A2ML -> SCM shadow sync Root `+Cargo.toml+` defines the workspace. Members include: clinician, hardware-crash-team, contracts-rust, displace, personal-sysadmin, -panoptes, czech-file-knife (and sub-crates: cfk-core, cfk-cli, -cfk-search, cfk-vfs, cfk-providers, cfk-integrations, cfk-ios, -cfk-cache). +panoptes. czech-file-knife (the cfk-* crates) moved out on 2026-10-09 +to its own repo, https://github.com/hyperpolymath/czech-file-knife. ==== Elixir Components diff --git a/stapeln.toml b/stapeln.toml index 70dab7ea..8d334976 100644 --- a/stapeln.toml +++ b/stapeln.toml @@ -121,33 +121,6 @@ cmd = ["scan"] user = "nonroot" labels = { "org.opencontainers.image.title" = "Hardware Crash Team", "org.opencontainers.image.description" = "Hardware diagnostic and remediation for zombie devices, driver conflicts, PCI bus issues", "org.opencontainers.image.source" = "https://github.com/hyperpolymath/ambientops", "org.opencontainers.image.licenses" = "MPL-2.0" } -# ══════════════════════════════════════════════════════════════════ -# Service: czech-file-knife (Rust workspace — universal file tool) -# Binary: cfk -# Deps: own workspace (cfk-core, cfk-providers, cfk-cli, etc.) -# ══════════════════════════════════════════════════════════════════ - -[layers.cfk-build] -description = "Build czech-file-knife cfk-cli binary" -extends = "rust-toolchain" -packages = ["fuse3-dev"] -commands = [ - "mkdir -p /build", - "cp -r czech-file-knife/ /build/cfk/", - "cd /build/cfk && cargo build --release -p cfk-cli --locked", -] - -[layers.cfk-runtime] -description = "Czech File Knife minimal runtime" -extends = "wolfi-base" -packages = ["fuse3", "fuse3-libs", "ca-certificates"] -copy-from = [ - { layer = "cfk-build", src = "/build/cfk/target/release/cfk", dst = "/usr/local/bin/cfk" }, -] -entrypoint = ["/usr/local/bin/cfk"] -user = "cfk" -labels = { "org.opencontainers.image.title" = "Czech File Knife", "org.opencontainers.image.description" = "Universal file management tool with cloud providers, VFS, and search", "org.opencontainers.image.source" = "https://github.com/hyperpolymath/czech-file-knife", "org.opencontainers.image.licenses" = "MPL-2.0" } - # ══════════════════════════════════════════════════════════════════ # Service: panoptes (Rust — AI file scanner/renamer) # Binary: panoptes @@ -461,7 +434,6 @@ description = "All services with debug logging" layers = [ "clinician-build", "hct-build", - "cfk-build", "panoptes-build", "displace-build", "observatory-build", @@ -476,7 +448,6 @@ description = "All runtime-optimised service layers" layers = [ "clinician-runtime", "hct-runtime", - "cfk-runtime", "panoptes-runtime", "displace-runtime", "observatory-runtime", @@ -493,7 +464,6 @@ description = "Build layers for CI/CD test matrix" layers = [ "clinician-build", "hct-build", - "cfk-build", "panoptes-build", "displace-build", "observatory-build", @@ -512,9 +482,8 @@ layers = [ env = { LOG_LEVEL = "info", RUST_LOG = "info" } [targets.file-tools] -description = "File management: czech-file-knife + panoptes + displace" +description = "File management: panoptes + displace (czech-file-knife builds its own image: hyperpolymath/czech-file-knife)" layers = [ - "cfk-runtime", "panoptes-runtime", "displace-runtime", ]