├── .cargo └── config.toml ├── .dockerignore ├── .github └── workflows │ ├── ocaml.yml │ ├── publish-latest-image.yml │ ├── publish-release-image.yml │ ├── release-node.yml │ ├── release-python.yml │ ├── release-rust.yml │ └── release.yml ├── .gitignore ├── .ocamlformat ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin ├── dune ├── log.ml ├── main.ml └── util.ml ├── client ├── go │ ├── README.md │ ├── go.mod │ ├── wasmstore.go │ └── wasmstore_test.go ├── js │ ├── README.md │ ├── example.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── wasmstore.js ├── python │ ├── README.md │ ├── poetry.lock │ ├── pyproject.toml │ ├── test │ │ └── test_client.py │ └── wasmstore │ │ ├── __init__.py │ │ └── wasmstore.py └── rust │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── hash.rs │ ├── lib.rs │ └── path.rs ├── docker-compose.yml ├── dune ├── dune-project ├── scripts ├── cert.sh ├── fill.sh └── wasmstore.service ├── src ├── branch.ml ├── diff.ml ├── dune ├── error.ml ├── gc.ml ├── lib.rs ├── rust.ml ├── schema.ml ├── server.ml ├── server_websocket.ml ├── store.ml ├── wasmstore.ml └── wasmstore.mli ├── test ├── a.wasm ├── b.wasm ├── dune ├── wasmstore.ml └── wasmstore.t ├── wasmstore.opam └── wasmstore.opam.locked /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"] 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | _build 2 | target 3 | client 4 | duniverse 5 | -------------------------------------------------------------------------------- /.github/workflows/ocaml.yml: -------------------------------------------------------------------------------- 1 | name: 'OCaml tests' 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: '${{ matrix.os }}' 15 | steps: 16 | - name: 'Install deps' 17 | run: bash -c '''case "$(uname)" in 18 | (*Linux*) sudo apt update -y && sudo apt-get install -y libev-dev libssl-dev pkg-config; ;; 19 | (*Darwin*) brew install libev openssl pkg-config; ;; 20 | esac''' 21 | - name: 'Checkout code' 22 | uses: actions/checkout@v3 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | override: true 28 | - name: Cache Rust environment 29 | uses: Swatinem/rust-cache@v1 30 | - id: wasmstore-opam-cache 31 | name: 'OCaml/Opam cache' 32 | uses: actions/cache@v3 33 | with: 34 | key: 'wasmstore-opam-${{ matrix.ocaml-compiler }}-${{ matrix.os }}' 35 | path: ~/.opam 36 | - id: wasmstore-dune-cache 37 | name: 'OCaml/Dune cache' 38 | uses: actions/cache@v3 39 | with: 40 | key: wasmstore-dune-${{ matrix.ocaml-compiler }}-${{ matrix.os }}-${{ hashFiles('src/**') }}-${{ hashFiles('dune-project') }} 41 | path: _build 42 | - name: 'Use OCaml ${{ matrix.ocaml-compiler }}' 43 | uses: ocaml/setup-ocaml@v2 44 | with: 45 | ocaml-compiler: '${{ matrix.ocaml-compiler }}' 46 | - run: opam install . --deps-only -y 47 | - name: 'Run OCaml tests' 48 | run: opam exec -- dune runtest 49 | - name: Generate executable 50 | run: | 51 | opam exec -- dune build 52 | - name: Save wasmstore executable 53 | uses: actions/upload-artifact@v3 54 | with: 55 | name: wasmstore-${{ runner.platform }} 56 | path: | 57 | _build/install/default/bin/wasmstore 58 | strategy: 59 | fail-fast: true 60 | matrix: 61 | ocaml-compiler: 62 | - 5.1 63 | os: 64 | - macos-latest 65 | - ubuntu-latest 66 | -------------------------------------------------------------------------------- /.github/workflows/publish-latest-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish latest image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | VERSION: ${{ github.ref_name }} 10 | 11 | jobs: 12 | push_to_ghcr: 13 | name: Push to GitHub container registry 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | steps: 19 | - name: Check out the repo 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | 25 | # push to github container registry 26 | - name: Log in to github registry 27 | uses: docker/login-action@v2 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Build, tag, & push the latest image 34 | uses: docker/build-push-action@v4 35 | with: 36 | context: . 37 | push: true 38 | tags: ghcr.io/dylibso/wasmstore:latest 39 | cache-from: type=gha 40 | cache-to: type=gha,mode=max 41 | 42 | # push to dockerhub 43 | - name: Log in to dockerhub registry 44 | uses: docker/login-action@v2 45 | with: 46 | username: dylibso 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | 49 | - name: Build, tag, & push the latest image 50 | uses: docker/build-push-action@v4 51 | with: 52 | context: . 53 | push: true 54 | tags: dylibso/wasmstore:latest 55 | cache-from: type=gha 56 | cache-to: type=gha,mode=max 57 | -------------------------------------------------------------------------------- /.github/workflows/publish-release-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish tagged release image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | env: 9 | VERSION: ${{ github.ref_name }} 10 | 11 | jobs: 12 | push_to_ghcr: 13 | name: Push to GitHub container registry 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | steps: 19 | - name: Check out the repo 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | 25 | # push to github container registry 26 | - name: Log in to github registry 27 | uses: docker/login-action@v2 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: define TAG for release 34 | run: echo "GHCR_TAG=ghcr.io/dylibso/wasmstore:${VERSION#v}" >> $GITHUB_ENV 35 | 36 | - name: Build, tag, & push the latest image 37 | uses: docker/build-push-action@v4 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ env.GHCR_TAG }} 42 | cache-from: type=gha 43 | cache-to: type=gha,mode=max 44 | 45 | # push to dockerhub 46 | - name: Log in to dockerhub registry 47 | uses: docker/login-action@v2 48 | with: 49 | username: dylibso 50 | password: ${{ secrets.DOCKERHUB_TOKEN }} 51 | 52 | - name: define TAG for release 53 | run: echo "DOCKERHUB_TAG=dylibso/wasmstore:${VERSION#v}" >> $GITHUB_ENV 54 | 55 | - name: Build, tag, & push the latest image 56 | uses: docker/build-push-action@v4 57 | with: 58 | context: . 59 | push: true 60 | tags: ${{ env.DOCKERHUB_TAG }} 61 | cache-from: type=gha 62 | cache-to: type=gha,mode=max 63 | -------------------------------------------------------------------------------- /.github/workflows/release-node.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Node client' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | exe: 8 | name: Release Node client 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Node env 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | registry-url: "https://registry.npmjs.org" 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }} 21 | CI: true 22 | 23 | - name: Release Node Host SDK 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }} 26 | CI: true 27 | run: | 28 | cd client/js 29 | npm publish --access=public -------------------------------------------------------------------------------- /.github/workflows/release-python.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Python client' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | exe: 8 | name: Release Python client 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Python env 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.10" 18 | check-latest: true 19 | 20 | - name: Run image 21 | uses: abatilo/actions-poetry@v2 22 | 23 | - name: Build Python Host SDK 24 | run: | 25 | cd client/python 26 | cp ../../LICENSE . 27 | poetry install --no-dev 28 | poetry build 29 | 30 | - name: Release Python Host SDK 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: ${{ secrets.PYPI_API_USER }} 34 | password: ${{ secrets.PYPI_API_TOKEN }} 35 | packages_dir: client/python/dist/ 36 | -------------------------------------------------------------------------------- /.github/workflows/release-rust.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Rust client' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | exe: 8 | name: Release Rust client 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout sources 12 | uses: actions/checkout@v2 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | - run: | 20 | cd client/rust 21 | cargo publish --token ${CARGO_TOKEN} 22 | env: 23 | CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: created 7 | jobs: 8 | exe: 9 | name: Release executable 10 | runs-on: '${{ matrix.os }}' 11 | steps: 12 | - name: 'Install deps' 13 | run: bash -c '''case "$(uname)" in 14 | (*Linux*) sudo apt update -y && sudo apt-get install -y libev-dev libssl-dev pkg-config; ;; 15 | (*Darwin*) brew install libev openssl pkg-config; ;; 16 | esac''' 17 | - name: 'Checkout code' 18 | uses: actions/checkout@v3 19 | - name: Install Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | override: true 24 | - name: Cache Rust environment 25 | uses: Swatinem/rust-cache@v1 26 | - id: wasmstore-opam-cache 27 | name: 'OCaml/Opam cache' 28 | uses: actions/cache@v3 29 | with: 30 | key: 'wasmstore-opam-${{ matrix.ocaml-compiler }}-${{ matrix.target }}-${{ matrix.os }}' 31 | path: ~/.opam 32 | - id: wasmstore-dune-cache 33 | name: 'OCaml/Dune cache' 34 | uses: actions/cache@v3 35 | with: 36 | key: wasmstore-dune-${{ matrix.ocaml-compiler }}-${{ matrix.target }}-${{ matrix.os }}-${{ hashFiles('src/**') }}-${{ hashFiles('dune-project') }} 37 | path: _build 38 | - name: 'Use OCaml ${{ matrix.ocaml-compiler }}' 39 | uses: ocaml/setup-ocaml@v2 40 | with: 41 | ocaml-compiler: '${{ matrix.ocaml-compiler }}' 42 | - run: opam install . --deps-only -y 43 | - name: 'Make release' 44 | run: | 45 | version="${{ github.ref }}" 46 | if [[ "$version" = "refs/heads/main" ]]; then 47 | version="main" 48 | else 49 | version="${version/refs\/tags\/v/}" 50 | fi 51 | opam exec -- make release VERSION=${version} TARGET=${{ matrix.target }} 52 | - name: Upload Artifact to Release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | files: | 56 | *.tar.gz 57 | *.checksum.txt 58 | strategy: 59 | fail-fast: true 60 | matrix: 61 | ocaml-compiler: 62 | - 5.1 63 | os: 64 | - macos-latest 65 | - ubuntu-latest 66 | target: [x86_64, aarch64] 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .wasmstore 2 | *.wasm 3 | target 4 | Cargo.lock 5 | node_modules 6 | __pycache__ 7 | _build 8 | _opam 9 | # Added by cargo 10 | 11 | /target 12 | duniverse -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.26.1 2 | doc-comments = after-when-possible 3 | doc-comments-padding = 2 4 | doc-comments-tag-only = default 5 | parse-docstrings = true 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "2.5.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "1.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 28 | 29 | [[package]] 30 | name = "equivalent" 31 | version = "1.0.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 34 | 35 | [[package]] 36 | name = "hashbrown" 37 | version = "0.14.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 40 | dependencies = [ 41 | "ahash", 42 | ] 43 | 44 | [[package]] 45 | name = "indexmap" 46 | version = "2.0.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" 49 | dependencies = [ 50 | "equivalent", 51 | "hashbrown", 52 | ] 53 | 54 | [[package]] 55 | name = "once_cell" 56 | version = "1.19.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 59 | 60 | [[package]] 61 | name = "proc-macro2" 62 | version = "1.0.82" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 65 | dependencies = [ 66 | "unicode-ident", 67 | ] 68 | 69 | [[package]] 70 | name = "quote" 71 | version = "1.0.36" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 74 | dependencies = [ 75 | "proc-macro2", 76 | ] 77 | 78 | [[package]] 79 | name = "semver" 80 | version = "1.0.20" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 83 | 84 | [[package]] 85 | name = "syn" 86 | version = "2.0.61" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" 89 | dependencies = [ 90 | "proc-macro2", 91 | "quote", 92 | "unicode-ident", 93 | ] 94 | 95 | [[package]] 96 | name = "unicode-ident" 97 | version = "1.0.12" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 100 | 101 | [[package]] 102 | name = "version_check" 103 | version = "0.9.4" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 106 | 107 | [[package]] 108 | name = "wasm" 109 | version = "0.1.0" 110 | dependencies = [ 111 | "wasmparser", 112 | ] 113 | 114 | [[package]] 115 | name = "wasmparser" 116 | version = "0.206.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "39192edb55d55b41963db40fd49b0b542156f04447b5b512744a91d38567bdbc" 119 | dependencies = [ 120 | "ahash", 121 | "bitflags", 122 | "hashbrown", 123 | "indexmap", 124 | "semver", 125 | ] 126 | 127 | [[package]] 128 | name = "zerocopy" 129 | version = "0.7.34" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 132 | dependencies = [ 133 | "zerocopy-derive", 134 | ] 135 | 136 | [[package]] 137 | name = "zerocopy-derive" 138 | version = "0.7.34" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 141 | dependencies = [ 142 | "proc-macro2", 143 | "quote", 144 | "syn", 145 | ] 146 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["staticlib", "cdylib"] 8 | 9 | [dependencies] 10 | wasmparser = "0.206" 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest as rust 2 | LABEL org.opencontainers.image.source=https://github.com/dylibso/wasmstore 3 | LABEL org.opencontainers.image.description="Wasmstore image" 4 | LABEL org.opencontainers.image.licenses=BSD-3-Clause 5 | 6 | FROM ocaml/opam:debian-12-ocaml-5.2 as build 7 | RUN sudo apt-get update && sudo apt-get install -y libgmp-dev pkg-config libssl-dev libffi-dev curl 8 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 9 | COPY --chown=opam . /home/opam/src 10 | COPY --chown=opam --from=rust /usr/local/cargo /home/opam/.cargo 11 | COPY --chown=opam --from=rust /usr/local/cargo/bin/rustc /usr/local/bin/rustc 12 | RUN sudo ln -sf /home/opam/.cargo/bin/cargo /usr/bin/cargo 13 | RUN sudo ln -sf /home/opam/.cargo/bin/rustc /usr/bin/rustc 14 | WORKDIR /home/opam/src 15 | RUN opam repository add opam-repository git+https://github.com/ocaml/opam-repository.git 16 | RUN opam update -y 17 | RUN opam install -j 1 dune -y 18 | RUN opam install -j $(nproc) opam-monorepo -y 19 | RUN opam repository add dune-universe git+https://github.com/dune-universe/opam-overlays.git 20 | RUN opam monorepo pull 21 | RUN eval $(opam env) && dune build -j $(nproc) ./bin/main.exe 22 | 23 | FROM debian:12 24 | ENV PORT=6384 25 | ENV HOST=0.0.0.0 26 | COPY --from=build /usr/lib /usr/lib 27 | COPY --from=build /home/opam/src/_build/default/bin/main.exe /usr/bin/wasmstore 28 | RUN groupadd -r wasmstore && useradd -m -r -g wasmstore wasmstore 29 | USER wasmstore 30 | WORKDIR /home/wasmstore 31 | EXPOSE ${PORT} 32 | RUN mkdir -p /home/wasmstore/db 33 | CMD wasmstore server --root /home/wasmstore/db --host ${HOST} --port ${PORT} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | VERSION?=main 3 | TARGET?=$(shell uname -m) 4 | UNAME_S:=$(shell uname -s | tr '[:upper:]' '[:lower:]') 5 | ifeq ($(UNAME_S),darwin) 6 | UNAME_S=macos 7 | endif 8 | RELEASE_DIR=wasmstore-$(TARGET)-$(UNAME_S)-$(VERSION) 9 | 10 | build: 11 | dune build 12 | 13 | clean: 14 | cargo clean 15 | dune clean 16 | 17 | install: 18 | mkdir -p $(PREFIX)/bin 19 | cp _build/default/bin/main.exe $(PREFIX)/bin/wasmstore 20 | 21 | uninstall: 22 | rm -f $(PREFIX)/bin/wasmstore 23 | 24 | service: 25 | mkdir -p ~/.config/systemd/user 26 | cp scripts/wasmstore.service ~/.config/systemd/user/wasmstore.service 27 | systemctl --user daemon-reload 28 | @echo 'To start the wasmstore service run: systemctml --user start wasmstore' 29 | 30 | release: build 31 | rm -rf $(RELEASE_DIR) 32 | mkdir -p $(RELEASE_DIR) 33 | cp _build/install/default/bin/wasmstore $(RELEASE_DIR) 34 | cp scripts/wasmstore.service $(RELEASE_DIR) 35 | cp README.md $(RELEASE_DIR) 36 | cp LICENSE $(RELEASE_DIR) 37 | tar czfv $(RELEASE_DIR).tar.gz $(RELEASE_DIR) 38 | shasum -a 256 $(RELEASE_DIR).tar.gz > $(RELEASE_DIR).checksum.txt 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasmstore 2 | 3 | [![Latest github release](https://img.shields.io/github/v/release/dylibso/wasmstore?include_prereleases&label=latest)](https://github.com/dylibso/wasmstore/releases/latest) 4 | [![npm](https://img.shields.io/npm/v/@dylibso/wasmstore)](https://www.npmjs.com/package/@dylibso/wasmstore) 5 | [![pypi](https://img.shields.io/pypi/v/wasmstore)](https://pypi.org/project/wasmstore/) 6 | [![crates.io](https://img.shields.io/crates/v/wasmstore-client)](https://crates.io/crates/wasmstore-client) 7 | 8 | A content-addressable store for WASM modules 9 | 10 | - Built-in WASM validation 11 | - History management, branching and merging 12 | - Command-line interface 13 | - HTTP interface 14 | - Simple authentication with roles based on HTTP request methods 15 | - Optional SSL 16 | 17 | ## Overview 18 | 19 | - WebAssembly modules are identified by their `hash` and are associated with a `path`, similar to a path on disk 20 | - Storing modules based on their hashes allows wasmstore to de-duplicate identical modules 21 | - Paths make it possible to give modules custom names 22 | - Any time the store is modified a `commit` is created 23 | - Commits can be identified by a hash and represent a checkpoint of the store 24 | - Contains some additional metadata: author, message and timestamp 25 | - Every time an existing `path` is updated a new `version` is created automatically 26 | - `rollback` reverts a path to the previous version 27 | - `restore` reverts to any prior `commit` 28 | - `versions` lists the history for a path, including module hashses and commit hashes 29 | - A `branch` can be helpful for testing, to update several paths at once or for namespacing 30 | - The `main` branch is the default, but it's possible to create branches with any name 31 | - `snapshot` gets the current `commit` hash 32 | - `merge` is used to merge a `branch` into another 33 | 34 | ## Building 35 | 36 | ### Docker 37 | 38 | There is a `Dockerfile` at the root of the repository that can be used to build and run the `wasmstore` server: 39 | 40 | ```shell 41 | $ docker build -t wasmstore . 42 | $ docker run -it -p 6384:6384 wasmstore 43 | ``` 44 | 45 | ## Pull from registry (Docker Hub or GitHub Registry) 46 | ```sh 47 | $ docker pull dylibso/wasmstore 48 | $ docker run --rm -it -p 6384:6384 dylibso/wasmstore 49 | ``` 50 | 51 | ### Opam 52 | 53 | The `wasmstore` executable contains the command-line interface and the server, to build it you will need [opam](https://opam.ocaml.org) 54 | installed. 55 | 56 | ```shell 57 | $ opam install . --deps-only 58 | $ dune build 59 | $ dune exec ./bin/main.exe --help 60 | ``` 61 | 62 | `wasmstore` can also be built using [opam-monorepo](https://github.com/tarides/opam-monorepo): 63 | 64 | ```shell 65 | $ opam repository add dune-universe git+https://github.com/dune-universe/opam-overlays.git 66 | $ opam install opam-monorepo 67 | $ opam monorepo pull 68 | $ dune build ./bin 69 | $ dune exec ./bin/main.exe --help 70 | ``` 71 | 72 | ## Installation 73 | 74 | Once `wasmstore` has been built it can be installed with: 75 | 76 | ```sh 77 | $ make PREFIX=/usr/local install 78 | ``` 79 | 80 | ## HTTP Interface 81 | 82 | The server can be started using the `wasmstore` executable: 83 | 84 | ```sh 85 | $ wasmstore server 86 | ``` 87 | 88 | or `docker-compose`: 89 | 90 | ```sh 91 | $ docker-compose up 92 | ``` 93 | 94 | All endpoints except the `/branch` endpoints accept a header named `Wasmstore-Branch` 95 | that will cause the call to modify the specified branch instead of the default 96 | branch. 97 | 98 | - `GET /api/v1/modules/*` 99 | - Returns a JSON object mapping module paths to their hashes for the 100 | specified path 101 | - Example: `curl http://127.0.0.1:6384/api/v1/modules` 102 | - `GET /api/v1/module/*` 103 | - Get a single module by hash or path, the module hash will also be stored in 104 | the `Wasmstore-Hash` header in the response. 105 | - Example: `curl http://127.0.0.1:6384/api/v1/module/mymodule.wasm` 106 | - `HEAD /api/v1/module/*` 107 | - Returns `200` status code if the path exists, otherwise `404` 108 | - `POST /api/v1/module/*` 109 | - Add a new module, the request body should contain the WASM module 110 | - Example: `curl --data-binary @mymodule.wasm http://127.0.0.1:6384/api/v1/module/mymodule.wasm` 111 | - `DELETE /api/v1/module/*` 112 | - Delete a module by hash or path 113 | - `GET /api/v1/hash/*` 114 | - Get the hash of the module stored at a specific path 115 | - `POST /api/v1/hash/:hash/*` 116 | - Set the path to point to the provided hash (the hash should already exist in the store) 117 | - `PUT /api/v1/branch/:branch` 118 | - Switch the default branch 119 | - `POST /api/v1/branch/:branch` 120 | - Create a new branch 121 | - `DELETE /api/v1/branch/:branch` 122 | - Delete a branch 123 | - `GET /api/v1/branch` 124 | - Return the name of the default branch 125 | - `GET /api/v1/branches` 126 | - Return a JSON array of active branch names 127 | - `POST /api/v1/gc` 128 | - Run garbage collection 129 | - `POST /api/v1/merge/:branch` 130 | - Merge the specified branch into the default branch 131 | - `POST /api/v1/restore/:hash/*` 132 | - Revert to the specified commit hash 133 | - It's possible to revert the entire tree or a single path 134 | - `POST /api/v1/rollback/*` 135 | - Revert to the last commit 136 | - This can also be used to revert the entire tree or a single path 137 | - `GET /api/v1/snapshot` 138 | - Returns the latest commit hash 139 | - `GET /api/v1/commit/:hash` 140 | - Returns a JSON object with information about a commit 141 | - `GET /api/v1/versions/*` 142 | - Returns an array of pairs (module hash, commit hash) of all previous modules stored at the provided path 143 | - `GET /api/v1/version/:index/*` 144 | - Returns a previous version of a module at the provided path 145 | - `GET /api/v1/watch` 146 | - A WebSocket endpoint that sends updates about changes to the store to the client 147 | - `* /api/v1/auth` 148 | - This endpoint can be used with any method to check capabilities for an authentication secret 149 | 150 | There are existing clients for [Rust](https://github.com/dylibso/wasmstore/tree/main/client/rust), [Javascript](https://github.com/dylibso/wasmstore/tree/main/client/js) 151 | [Python](https://github.com/dylibso/wasmstore/tree/main/client/python) and [Go](https://github.com/dylibso/wasmstore/tree/main/client/go) 152 | 153 | ### Authentication 154 | 155 | Using the `wasmstore server --auth` flag or the `WASMSTORE_AUTH` environment variable you can restrict certain authentication keys 156 | to specific request methods: 157 | 158 | ```sh 159 | $ wasmstore server --auth "MY_SECRET_KEY:GET,POST;MY_SECRET_READONLY_KEY:GET" 160 | $ WASMSTORE_AUTH="MY_SECRET_KEY:GET,POST;MY_SECRET_READONLY_KEY:GET" wasmstore server 161 | ``` 162 | 163 | On the client side you should supply the key using the `Wasmstore-Auth` header 164 | 165 | ## Command line 166 | 167 | See the output of `wasmstore --help` for a full list of commands 168 | 169 | ### Examples 170 | 171 | Add a file from disk 172 | 173 | ```sh 174 | $ wasmstore add /path/to/myfile.wasm 175 | ``` 176 | 177 | Get module using hash: 178 | 179 | ```sh 180 | $ wasmstore find 181 | ``` 182 | 183 | Get module using path: 184 | 185 | ```sh 186 | $ wasmstore find myfile.wasm 187 | ``` 188 | 189 | Create a new branch: 190 | 191 | ```sh 192 | $ wasmstore branch my-branch 193 | ``` 194 | 195 | Add module to non-default branch 196 | 197 | ```sh 198 | $ wasmstore add --branch my-branch /path/to/another-file.wasm 199 | ``` 200 | 201 | Merge a branch into the main branch 202 | 203 | ```sh 204 | $ wasmstore merge my-branch 205 | ``` 206 | 207 | Delete a branch 208 | 209 | ```sh 210 | $ wasmstore branch my-branch --delete 211 | ``` 212 | 213 | Get the current commit hash: 214 | 215 | ```sh 216 | $ wasmstore snapshot 217 | ``` 218 | 219 | Restore to a prior commit: 220 | 221 | ```sh 222 | $ wasmstore restore 223 | ``` 224 | 225 | Run garbage collection: 226 | 227 | ```sh 228 | $ wasmstore gc 229 | ``` 230 | 231 | Export the main branch to a directory on disk: 232 | 233 | ```sh 234 | $ wasmstore export -o ./wasm-modules 235 | ``` 236 | 237 | Backup the entire database: 238 | 239 | ```sh 240 | $ wasmstore backup backup.tar.gz 241 | ``` 242 | 243 | To create a new store from a backup: 244 | 245 | ```sh 246 | $ mkdir $WASMSTORE_ROOT 247 | $ cd $WASMSTORE_ROOT 248 | $ tar xzf /path/to/backup.tar.gz 249 | ``` 250 | 251 | ## A note on garbage collection 252 | 253 | When the gc is executed for a branch all prior commits are squashed into one 254 | and all non-reachable objects are removed. For example, if an object is still 255 | reachable from another branch it will not be deleted. Because of this, running 256 | the garbage collector may purge prior commits, potentially causing `restore` 257 | to fail. 258 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name wasmstore) 3 | (name main) 4 | (modes exe) 5 | (libraries wasmstore cmdliner eio_main)) 6 | -------------------------------------------------------------------------------- /bin/log.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Wasmstore 3 | open Cmdliner 4 | open Util 5 | 6 | let log store = 7 | let plain = 8 | let doc = Arg.info ~doc:"Show plain text without pager" [ "plain" ] in 9 | Arg.(value & flag & doc) 10 | in 11 | let pager = 12 | let doc = Arg.info ~doc:"Specify pager program to use" [ "pager" ] in 13 | Arg.(value & opt string "pager" & doc) 14 | in 15 | let num = 16 | let doc = Arg.info ~doc:"Number of entries to show" [ "n"; "max-count" ] in 17 | Arg.(value & opt (some int) None & doc) 18 | in 19 | let skip = 20 | let doc = Arg.info ~doc:"Number of entries to skip" [ "skip" ] in 21 | Arg.(value & opt (some int) None & doc) 22 | in 23 | let reverse = 24 | let doc = Arg.info ~doc:"Print in reverse order" [ "reverse" ] in 25 | Arg.(value & flag & doc) 26 | in 27 | let exception Return in 28 | let cmd store plain pager num skip reverse = 29 | run @@ fun env -> 30 | let* t = store env in 31 | let repo = repo t in 32 | let skip = ref (Option.value ~default:0 skip) in 33 | let num = Option.value ~default:0 num in 34 | let num_count = ref 0 in 35 | let commit formatter key = 36 | if num > 0 && !num_count >= num then raise Return 37 | else if !skip > 0 then 38 | let () = decr skip in 39 | Lwt.return_unit 40 | else 41 | let+ commit = Store.Commit.of_key repo key in 42 | let hash = Store.Backend.Commit.Key.to_hash key in 43 | let info = Store.Commit.info (Option.get commit) in 44 | let date = Store.Info.date info in 45 | let author = Store.Info.author info in 46 | let message = Store.Info.message info in 47 | let () = 48 | Fmt.pf formatter "commit %a\nAuthor: %s\nDate: %s\n\n%s\n\n%!" 49 | (Irmin.Type.pp Store.hash_t) 50 | hash author (convert_date date) message 51 | in 52 | incr num_count 53 | in 54 | let* x = Store.Head.get (Wasmstore.store t) in 55 | let max = [ `Commit (Store.Commit.key x) ] in 56 | let iter ~commit ~max repo = 57 | Lwt.catch 58 | (fun () -> 59 | if reverse then Store.Repo.iter ~commit ~min:[] ~max repo 60 | else Store.Repo.breadth_first_traversal ~commit ~max repo) 61 | (function Return -> Lwt.return_unit | exn -> raise exn) 62 | in 63 | if plain then 64 | let commit = commit Format.std_formatter in 65 | iter ~commit ~max repo 66 | else 67 | Lwt.catch 68 | (fun () -> 69 | let out = Unix.open_process_out pager in 70 | let commit = commit (Format.formatter_of_out_channel out) in 71 | let+ () = iter ~commit ~max repo in 72 | let _ = Unix.close_process_out out in 73 | ()) 74 | (function 75 | | Sys_error s when String.equal s "Broken pipe" -> Lwt.return_unit 76 | | exn -> raise exn) 77 | in 78 | let doc = "list all commits in order" in 79 | let info = Cmd.info "log" ~doc in 80 | let term = Term.(const cmd $ store $ plain $ pager $ num $ skip $ reverse) in 81 | Cmd.v info term 82 | -------------------------------------------------------------------------------- /bin/main.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Wasmstore 3 | open Cmdliner 4 | open Util 5 | 6 | let ( // ) = Filename.concat 7 | let null_formatter = Format.make_formatter (fun _ _ _ -> ()) (fun () -> ()) 8 | let eio_linux_src = "eio_linux" 9 | let string_of_level = Fmt.to_to_string Logs.pp_level 10 | let warning_level = string_of_level Logs.Warning 11 | 12 | let reporter ppf = 13 | let report src level ~over k msgf = 14 | let name = Logs.Src.name src in 15 | let ppf = 16 | if name = eio_linux_src && string_of_level level = warning_level then 17 | null_formatter 18 | else ppf 19 | in 20 | let k _ = 21 | over (); 22 | k () 23 | in 24 | let with_metadata header _tags k ppf fmt = 25 | Format.kfprintf k ppf 26 | ("%a[%a]: " ^^ fmt ^^ "\n%!") 27 | Logs_fmt.pp_header (level, header) 28 | Fmt.(styled `Magenta string) 29 | name 30 | in 31 | msgf @@ fun ?header ?tags fmt -> with_metadata header tags k ppf fmt 32 | in 33 | { Logs.report } 34 | 35 | let setup_log style_renderer level = 36 | Fmt_tty.setup_std_outputs ?style_renderer (); 37 | Logs.set_level ~all:true level; 38 | Logs.set_reporter (reporter Fmt.stderr); 39 | () 40 | 41 | let setup_log = 42 | Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ()) 43 | 44 | let default_root = Filename.concat (Sys.getenv "HOME") ".wasmstore" 45 | 46 | let root = 47 | let doc = "root" in 48 | let env = Cmd.Env.info "WASMSTORE_ROOT" in 49 | Arg.(value & opt string default_root & info [ "root" ] ~docv:"PATH" ~doc ~env) 50 | 51 | let branch = 52 | let doc = "branch" in 53 | let env = Cmd.Env.info "WASMSTORE_BRANCH" in 54 | Arg.( 55 | value 56 | & opt string Store.Branch.main 57 | & info [ "branch" ] ~docv:"NAME" ~doc ~env) 58 | 59 | let author = 60 | let doc = "author" in 61 | let env = Cmd.Env.info "WASMSTORE_AUTHOR" in 62 | Arg.( 63 | value & opt (some string) None & info [ "author" ] ~docv:"NAME" ~doc ~env) 64 | 65 | let tls = 66 | let doc = "tls key file and certificate file" in 67 | let env = Cmd.Env.info "WASMSTORE_TLS" in 68 | Arg.( 69 | value 70 | & opt (some (pair ~sep:',' string string)) None 71 | & info [ "tls" ] ~docv:"KEY_FILE,CERT_FILE" ~doc ~env) 72 | 73 | let host = 74 | let doc = "hostname" in 75 | let env = Cmd.Env.info "WASMSTORE_HOST" in 76 | Arg.(value & opt (some string) None & info [ "host" ] ~docv:"HOST" ~doc ~env) 77 | 78 | let port = 79 | let doc = "port" in 80 | let env = Cmd.Env.info "WASMSTORE_PORT" in 81 | Arg.(value & opt (some int) None & info [ "port" ] ~docv:"PORT" ~doc ~env) 82 | 83 | let branch_from n = 84 | let doc = "branch to merge from" in 85 | Arg.(value & pos n string Store.Branch.main & info [] ~docv:"NAME" ~doc) 86 | 87 | let branch_name n = 88 | let doc = "branch name" in 89 | Arg.(value & pos n string Store.Branch.main & info [] ~docv:"NAME" ~doc) 90 | 91 | let hash n = 92 | let doc = "hash" in 93 | Arg.(value & pos n string "" & info [] ~docv:"HASH" ~doc) 94 | 95 | let delete_flag = 96 | let doc = "delete branch" in 97 | Arg.(value & flag & info [ "delete" ] ~doc) 98 | 99 | let list_flag = 100 | let doc = "list branches" in 101 | Arg.(value & flag & info [ "list" ] ~doc) 102 | 103 | let file p = 104 | let doc = "file" in 105 | Arg.(value & pos p string "" & info [] ~docv:"WASM" ~doc) 106 | 107 | let path p = 108 | let doc = "path" in 109 | Arg.(value & pos p (list ~sep:'/' string) [] & info [] ~docv:"PATH" ~doc) 110 | 111 | let path_opt p = 112 | let doc = "path" in 113 | Arg.( 114 | value & pos p (some (list ~sep:'/' string)) None & info [] ~docv:"PATH" ~doc) 115 | 116 | let auth = 117 | let doc = "auth" in 118 | let env = Cmd.Env.info "WASMSTORE_AUTH" in 119 | Arg.( 120 | value 121 | & opt (some (list ~sep:';' (pair ~sep:':' string string))) None 122 | & info ~env [ "auth" ] ~docv:"KEY:GET,POST;KEY1:GET" ~doc) 123 | 124 | let cors = 125 | let doc = "enable CORS" in 126 | Arg.(value & flag & info [ "cors" ] ~doc) 127 | 128 | let store = 129 | let aux () root branch author env = v ?author ~branch root ~env in 130 | Term.(const aux $ setup_log $ root $ branch $ author) 131 | 132 | let buf_size = 4096 133 | 134 | let rec read_file buf ic f = 135 | let* n = Lwt_io.read_into ic buf 0 buf_size in 136 | f (Some (Bytes.sub_string buf 0 n)); 137 | if n < buf_size then 138 | let () = f None in 139 | Lwt.return_unit 140 | else read_file buf ic f 141 | 142 | let file_stream filename = 143 | let buf = Bytes.create buf_size in 144 | let s, push = Lwt_stream.create () in 145 | let* () = 146 | Lwt_io.with_file ~mode:Input filename (fun ic -> read_file buf ic push) 147 | in 148 | Lwt.return s 149 | 150 | let stdin_stream () = 151 | let buf = Bytes.create buf_size in 152 | let s, push = Lwt_stream.create () in 153 | let* () = read_file buf Lwt_io.stdin push in 154 | Lwt.return s 155 | 156 | let add = 157 | let cmd store filename path = 158 | let path = 159 | match path with Some p -> p | None -> [ Filename.basename filename ] 160 | in 161 | run @@ fun env -> 162 | let* t = store env in 163 | let* data = 164 | if filename = "-" then stdin_stream () else file_stream filename 165 | in 166 | Lwt.catch 167 | (fun () -> 168 | let+ hash = import t path data in 169 | Format.printf "%a\n" (Irmin.Type.pp Store.hash_t) hash) 170 | (function 171 | | Validation_error msg -> 172 | Lwt_io.fprintlf Lwt_io.stderr "ERROR invalid module: %s" msg 173 | | exn -> raise exn) 174 | in 175 | 176 | let doc = "add a WASM module" in 177 | let info = Cmd.info "add" ~doc in 178 | let term = Term.(const cmd $ store $ file 0 $ path_opt 1) in 179 | Cmd.v info term 180 | 181 | let find = 182 | let cmd store path = 183 | run @@ fun env -> 184 | let* t = store env in 185 | let+ value = find t path in 186 | match value with None -> exit 1 | Some value -> print_string value 187 | in 188 | let doc = "find a WASM module by hash or name" in 189 | let info = Cmd.info "find" ~doc in 190 | let term = Term.(const cmd $ store $ path 0) in 191 | Cmd.v info term 192 | 193 | let filename = 194 | let cmd store path = 195 | run @@ fun env -> 196 | let* t = store env in 197 | let config = Store.Repo.config (repo t) in 198 | let root = Irmin.Backend.Conf.find_root config |> Option.get in 199 | let* opt = Wasmstore.get_hash_and_filename t path in 200 | match opt with 201 | | None -> exit 1 202 | | Some (_, filename) -> Lwt_io.printl (Filename.concat root filename) 203 | in 204 | let doc = "get the path on disk by hash or name" in 205 | let info = Cmd.info "filename" ~doc in 206 | let term = Term.(const cmd $ store $ path 0) in 207 | Cmd.v info term 208 | 209 | let remove = 210 | let cmd store path = 211 | run @@ fun env -> 212 | let* t = store env in 213 | Wasmstore.remove t path 214 | in 215 | let doc = "remove WASM module from store by hash" in 216 | let info = Cmd.info "remove" ~doc in 217 | let term = Term.(const cmd $ store $ path 0) in 218 | Cmd.v info term 219 | 220 | let merge = 221 | let cmd store branch_from = 222 | run @@ fun env -> 223 | let* t = store env in 224 | let+ res = merge t branch_from in 225 | match res with 226 | | Ok () -> () 227 | | Error e -> 228 | let stderr = Format.formatter_of_out_channel stderr in 229 | Format.fprintf stderr "ERROR %a" 230 | (Irmin.Type.pp Irmin.Merge.conflict_t) 231 | e 232 | in 233 | let doc = "merge branch into main" in 234 | let info = Cmd.info "merge" ~doc in 235 | let term = Term.(const cmd $ store $ branch_from 0) in 236 | Cmd.v info term 237 | 238 | let gc = 239 | let cmd store = 240 | run @@ fun env -> 241 | let* t = store env in 242 | let+ res = gc t in 243 | Printf.printf "%d\n" res 244 | in 245 | let doc = "cleanup modules that are no longer referenced" in 246 | let info = Cmd.info "gc" ~doc in 247 | let term = Term.(const cmd $ store) in 248 | Cmd.v info term 249 | 250 | let list = 251 | let cmd store path = 252 | run @@ fun env -> 253 | let* t = store env in 254 | let+ items = list t path in 255 | List.iter 256 | (fun (path, hash) -> 257 | Format.printf "%a\t%a\n" 258 | (Irmin.Type.pp Store.Hash.t) 259 | hash 260 | (Irmin.Type.pp Store.Path.t) 261 | path) 262 | items 263 | in 264 | let doc = "list WASM modules" in 265 | let info = Cmd.info "list" ~doc in 266 | let term = Term.(const cmd $ store $ path 0) in 267 | Cmd.v info term 268 | 269 | let snapshot = 270 | let cmd store = 271 | run @@ fun env -> 272 | let* t = store env in 273 | let+ head = snapshot t in 274 | print_endline (Irmin.Type.to_string Store.Hash.t (Store.Commit.hash head)) 275 | in 276 | let doc = "get current head commit hash" in 277 | let info = Cmd.info "snapshot" ~doc in 278 | let term = Term.(const cmd $ store) in 279 | Cmd.v info term 280 | 281 | let restore = 282 | let cmd store commit path = 283 | run @@ fun env -> 284 | let* t = store env in 285 | let hash = Irmin.Type.of_string Store.Hash.t commit in 286 | match hash with 287 | | Error _ -> 288 | Printf.fprintf stderr "ERROR invalid hash\n"; 289 | Lwt.return_unit 290 | | Ok hash -> ( 291 | let* commit = Store.Commit.of_hash (repo t) hash in 292 | match commit with 293 | | None -> 294 | Printf.fprintf stderr "ERROR invalid commit\n"; 295 | Lwt.return_unit 296 | | Some commit -> restore ?path t commit) 297 | in 298 | let doc = "restore to a previous commit" in 299 | let info = Cmd.info "restore" ~doc in 300 | let term = Term.(const cmd $ store $ hash 0 $ path_opt 1) in 301 | Cmd.v info term 302 | 303 | let rollback = 304 | let cmd store path = 305 | run @@ fun env -> 306 | let* t = store env in 307 | rollback ?path t 1 308 | in 309 | let doc = "rollback to the last commit" in 310 | let info = Cmd.info "rollback" ~doc in 311 | let term = Term.(const cmd $ store $ path_opt 0) in 312 | Cmd.v info term 313 | 314 | let contains = 315 | let cmd store path = 316 | run @@ fun env -> 317 | let* t = store env in 318 | let+ value = contains t path in 319 | Format.printf "%b\n" value 320 | in 321 | let doc = "check if a WASM module exists by hash or name" in 322 | let info = Cmd.info "contains" ~doc in 323 | let term = Term.(const cmd $ store $ path 0) in 324 | Cmd.v info term 325 | 326 | let set = 327 | let cmd store hash path = 328 | run @@ fun env -> 329 | let* t = store env in 330 | let hash' = Irmin.Type.of_string Store.Hash.t hash in 331 | match hash' with 332 | | Error _ -> Lwt_io.eprintlf "invalid hash: %s" hash 333 | | Ok hash -> Wasmstore.set t path hash 334 | in 335 | let doc = "set a path to point to an existing hash" in 336 | let info = Cmd.info "set" ~doc in 337 | let term = Term.(const cmd $ store $ hash 0 $ path 1) in 338 | Cmd.v info term 339 | 340 | let commit = 341 | let cmd store hash = 342 | run @@ fun env -> 343 | let* t = store env in 344 | let hash' = Irmin.Type.of_string Hash.t hash in 345 | let fail body = Lwt_io.fprintlf Lwt_io.stderr "ERROR %s" body in 346 | match hash' with 347 | | Error _ -> fail "invalid hash" 348 | | Ok hash -> ( 349 | let* info = commit_info t hash in 350 | match info with 351 | | Some info -> 352 | let body = 353 | Irmin.Type.to_json_string ~minify:false Commit_info.t info 354 | in 355 | Lwt_io.printl body 356 | | None -> fail "invalid commit") 357 | in 358 | let doc = "get commit info" in 359 | let info = Cmd.info "commit" ~doc in 360 | let term = Term.(const cmd $ store $ hash 0) in 361 | Cmd.v info term 362 | 363 | let hash = 364 | let cmd store path = 365 | run @@ fun env -> 366 | let* t = store env in 367 | let+ hash = Wasmstore.hash t path in 368 | match hash with 369 | | None -> exit 1 370 | | Some hash -> Format.printf "%a\n" (Irmin.Type.pp Store.Hash.t) hash 371 | in 372 | let doc = "Get the hash for the provided path" in 373 | let info = Cmd.info "hash" ~doc in 374 | let term = Term.(const cmd $ store $ path 0) in 375 | Cmd.v info term 376 | 377 | let server = 378 | let rec cmd store host port auth cors tls = 379 | let tls' = 380 | match tls with 381 | | Some (k, c) -> Some (`Key_file k, `Cert_file c) 382 | | None -> None 383 | in 384 | let* t = store in 385 | try Server.run ~cors ?tls:tls' ?host ?port ?auth t 386 | with exn -> 387 | Logs.err (fun l -> l "Server.run: %s" @@ Printexc.to_string exn); 388 | cmd store host port auth cors tls 389 | in 390 | let cmd store host port auth cors tls = 391 | run @@ fun env -> cmd (store env) host port auth cors tls 392 | in 393 | let doc = "Run server" in 394 | let info = Cmd.info "server" ~doc in 395 | let term = Term.(const cmd $ store $ host $ port $ auth $ cors $ tls) in 396 | Cmd.v info term 397 | 398 | let branch = 399 | let cmd () root branch_name delete list = 400 | run @@ fun env -> 401 | let* t = v root ~env in 402 | if list then 403 | let+ branches = Branch.list t in 404 | List.iter print_endline branches 405 | else if delete then Branch.delete t branch_name 406 | else 407 | let* _ = Error.unwrap_lwt @@ Branch.create t branch_name in 408 | Lwt.return_unit 409 | in 410 | let doc = "Modify a branch" in 411 | let info = Cmd.info "branch" ~doc in 412 | let term = 413 | Term.( 414 | const cmd $ setup_log $ root $ branch_name 0 $ delete_flag $ list_flag) 415 | in 416 | Cmd.v info term 417 | 418 | let run_command command diff = 419 | match command with 420 | | h :: t -> 421 | let s = Yojson.Safe.to_string diff in 422 | Lwt_process.pwrite (h, Array.of_list (h :: t)) s 423 | | [] -> Lwt_io.printlf "%s" (Yojson.Safe.to_string diff) 424 | 425 | let watch = 426 | let cmd store command = 427 | run @@ fun env -> 428 | let* t = store env in 429 | let* _w = watch t (run_command command) in 430 | let t, _ = Lwt.task () in 431 | t 432 | in 433 | let doc = "Print updates or run command when the store is updated" in 434 | let info = Cmd.info "watch" ~doc in 435 | let command = 436 | let doc = Arg.info ~docv:"COMMAND" ~doc:"Command to execute" [] in 437 | Arg.(value & pos_all string [] & doc) 438 | in 439 | let term = Term.(const cmd $ store $ command) in 440 | Cmd.v info term 441 | 442 | let audit = 443 | let cmd store path = 444 | run @@ fun env -> 445 | let* t = store env in 446 | let* lm = Store.last_modified (Wasmstore.store t) path in 447 | let* () = 448 | Lwt_list.iter_s 449 | (fun commit -> 450 | let info = Store.Commit.info commit in 451 | let hash = Store.Commit.hash commit in 452 | Lwt_io.printlf "%s\t%s\t%s" 453 | (convert_date @@ Store.Info.date info) 454 | (Store.Info.author info) 455 | (Irmin.Type.to_string Hash.t hash)) 456 | lm 457 | in 458 | Lwt.return_unit 459 | in 460 | let doc = "list commits that modified a specific path" in 461 | let info = Cmd.info "audit" ~doc in 462 | let term = Term.(const cmd $ store $ path 0) in 463 | Cmd.v info term 464 | 465 | let versions = 466 | let cmd store path = 467 | run @@ fun env -> 468 | let* t = store env in 469 | let* versions = versions t path in 470 | List.iter 471 | (fun (k, `Commit v) -> 472 | Fmt.pr "%a\tcommit: %a\n" (Irmin.Type.pp Hash.t) k 473 | (Irmin.Type.pp Hash.t) v) 474 | versions; 475 | Lwt.return_unit 476 | in 477 | let doc = "list previous versions of a path" in 478 | let info = Cmd.info "versions" ~doc in 479 | let term = Term.(const cmd $ store $ path 0) in 480 | Cmd.v info term 481 | 482 | let version = 483 | let cmd store path v = 484 | run @@ fun env -> 485 | let* t = store env in 486 | let+ version = version t path v in 487 | match version with 488 | | Some (k, `Commit v) -> 489 | Fmt.pr "%a\tcommit: %a\n" (Irmin.Type.pp Hash.t) k 490 | (Irmin.Type.pp Hash.t) v 491 | | None -> 492 | Fmt.pr "ERROR version %d does not exist for %a\n" v 493 | (Irmin.Type.pp Store.path_t) 494 | path 495 | in 496 | let doc = "get a past version of a plugin" in 497 | let info = Cmd.info "version" ~doc in 498 | let version = 499 | let doc = Arg.info ~docv:"VERSION" ~doc:"Version" [] in 500 | Arg.(value & pos 0 int 0 & doc) 501 | in 502 | let term = Term.(const cmd $ store $ path 1 $ version) in 503 | Cmd.v info term 504 | 505 | let backup = 506 | let cmd root output = 507 | let output = 508 | if Filename.is_relative output then Unix.getcwd () // output else output 509 | in 510 | Unix.chdir root; 511 | Unix.execvp "tar" [| "tar"; "czf"; output; "." |] 512 | in 513 | let doc = "create a tar backup of an entire store" in 514 | let info = Cmd.info "backup" ~doc in 515 | let output = Arg.(value & pos 0 string "" & info [] ~docv:"PATH" ~doc) in 516 | let term = Term.(const cmd $ root $ output) in 517 | Cmd.v info term 518 | 519 | let rec mkdir_all p = 520 | let parent = Filename.dirname p in 521 | let* parent_exists = Lwt_unix.file_exists parent in 522 | let* () = if not parent_exists then mkdir_all parent else Lwt.return_unit in 523 | Lwt.catch 524 | (fun () -> Lwt_unix.mkdir p 0o755) 525 | (function 526 | | Unix.Unix_error (Unix.EEXIST, _, _) -> Lwt.return_unit | e -> raise e) 527 | 528 | let export = 529 | let cmd store output = 530 | run @@ fun env -> 531 | let* t = store env in 532 | let repo = Wasmstore.repo t in 533 | let root = 534 | Irmin.Backend.Conf.get (Store.Repo.config repo) Irmin_fs.Conf.Key.root 535 | in 536 | let* files = Wasmstore.list t [] in 537 | Lwt_list.iter_p 538 | (fun (path, _) -> 539 | let* v = Wasmstore.get_hash_and_filename t path in 540 | match v with 541 | | Some (_, filename) -> 542 | let s = Lwt_io.chars_of_file (root // filename) in 543 | let out = output // Irmin.Type.to_string Store.path_t path in 544 | let parent = Filename.dirname out in 545 | let* () = mkdir_all parent in 546 | Lwt_io.chars_to_file out s 547 | | None -> Lwt.return_unit) 548 | files 549 | in 550 | let doc = "create a view on disk from a branch or commit" in 551 | let info = Cmd.info "export" ~doc in 552 | let output = 553 | let doc = "output path" in 554 | Arg.( 555 | value 556 | & opt string Store.Branch.main 557 | & info [ "output"; "o" ] ~docv:"OUTPUT" ~doc) 558 | in 559 | let term = Term.(const cmd $ store $ output) in 560 | Cmd.v info term 561 | 562 | let commands = 563 | Cmd.group (Cmd.info "wasmstore") 564 | [ 565 | add; 566 | find; 567 | remove; 568 | gc; 569 | list; 570 | contains; 571 | server; 572 | merge; 573 | branch; 574 | snapshot; 575 | restore; 576 | rollback; 577 | hash; 578 | watch; 579 | audit; 580 | versions; 581 | set; 582 | commit; 583 | filename; 584 | Log.log store; 585 | version; 586 | backup; 587 | export; 588 | ] 589 | 590 | let () = exit (Cmd.eval commands) 591 | -------------------------------------------------------------------------------- /bin/util.ml: -------------------------------------------------------------------------------- 1 | open Wasmstore 2 | 3 | let weekday Unix.{ tm_wday; _ } = 4 | match tm_wday with 5 | | 0 -> "Sun" 6 | | 1 -> "Mon" 7 | | 2 -> "Tue" 8 | | 3 -> "Wed" 9 | | 4 -> "Thu" 10 | | 5 -> "Fri" 11 | | 6 -> "Sat" 12 | | _ -> assert false 13 | 14 | let month Unix.{ tm_mon; _ } = 15 | match tm_mon with 16 | | 0 -> "Jan" 17 | | 1 -> "Feb" 18 | | 2 -> "Mar" 19 | | 3 -> "Apr" 20 | | 4 -> "May" 21 | | 5 -> "Jun" 22 | | 6 -> "Jul" 23 | | 7 -> "Aug" 24 | | 8 -> "Sep" 25 | | 9 -> "Oct" 26 | | 10 -> "Nov" 27 | | 11 -> "Dec" 28 | | _ -> assert false 29 | 30 | let convert_date timestamp = 31 | let date = Unix.localtime (Int64.to_float timestamp) in 32 | Fmt.str "%s %s %02d %02d:%02d:%02d %04d" (weekday date) (month date) 33 | date.tm_mday date.tm_hour date.tm_min date.tm_sec (date.tm_year + 1900) 34 | 35 | let run f = 36 | Eio_main.run @@ fun env -> 37 | Lwt_eio.with_event_loop ~clock:env#clock @@ fun _token -> 38 | Lwt_eio.run_lwt @@ fun () -> 39 | Error.catch_lwt 40 | (fun () -> f env) 41 | (fun err -> 42 | Logs.err (fun l -> l "%s" (Error.to_string err)); 43 | Lwt.return_unit) 44 | -------------------------------------------------------------------------------- /client/go/README.md: -------------------------------------------------------------------------------- 1 | # wasmstore 2 | 3 | A Go client for [wasmstore](https://github.com/dylibso/wasmstore) 4 | -------------------------------------------------------------------------------- /client/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dylibso/wasmstore/client/go 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /client/go/wasmstore.go: -------------------------------------------------------------------------------- 1 | package wasmstore 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | type Hash = string 13 | type CommitHash = string 14 | 15 | type Client struct { 16 | URL *url.URL 17 | Config *Config 18 | } 19 | 20 | type Config struct { 21 | Auth string 22 | Branch string 23 | Version string 24 | } 25 | 26 | type CommitInfo struct { 27 | Hash CommitHash `json:"hash"` 28 | Parents []CommitHash `json:"parents,omitempty"` 29 | Date int64 `json:"date"` 30 | Author string `json:"author"` 31 | Message string `json:"message"` 32 | } 33 | 34 | func JoinPath(path []string) string { 35 | return strings.Join(path[:], "/") 36 | } 37 | 38 | func SplitPath(path string) []string { 39 | if path == "/" || path == "" { 40 | return []string{} 41 | } 42 | return strings.Split(path, "/") 43 | } 44 | 45 | func NewClient(u string, config *Config) (Client, error) { 46 | version := "v1" 47 | if config != nil && config.Version != "" { 48 | version = config.Version 49 | } 50 | 51 | url, err := url.Parse(u + "/api/" + version) 52 | if err != nil { 53 | return Client{}, err 54 | } 55 | 56 | return Client{ 57 | URL: url, 58 | Config: config, 59 | }, nil 60 | } 61 | 62 | func (c *Client) Request(method string, route string, body io.Reader) ([]byte, int, error) { 63 | req, err := http.NewRequest(method, c.URL.String()+route, body) 64 | if err != nil { 65 | return nil, 0, err 66 | } 67 | 68 | if c.Config != nil { 69 | if c.Config.Auth != "" { 70 | req.Header.Add("Wasmstore-Auth", c.Config.Auth) 71 | } 72 | 73 | if c.Config.Branch != "" { 74 | req.Header.Add("Wasmstore-Branch", c.Config.Branch) 75 | } 76 | } 77 | 78 | res, err := http.DefaultClient.Do(req) 79 | if err != nil { 80 | return nil, 0, err 81 | } 82 | 83 | resBody, err := ioutil.ReadAll(res.Body) 84 | if err != nil { 85 | return nil, 0, err 86 | } 87 | 88 | res.Body.Close() 89 | return resBody, res.StatusCode, nil 90 | } 91 | 92 | func (c *Client) Find(path ...string) ([]byte, bool, error) { 93 | res, code, err := c.Request("GET", "/module/"+JoinPath(path), nil) 94 | if err != nil { 95 | return nil, false, err 96 | } 97 | 98 | return res, code == 200, nil 99 | } 100 | 101 | func (c *Client) Add(wasm io.Reader, path ...string) (Hash, error) { 102 | res, _, err := c.Request("POST", "/module/"+JoinPath(path), wasm) 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | return string(res), nil 108 | } 109 | 110 | func (c *Client) Set(wasm io.Reader, commit CommitHash, path ...string) (bool, error) { 111 | _, code, err := c.Request("POST", "/module/"+JoinPath(path), wasm) 112 | if err != nil { 113 | return false, err 114 | } 115 | 116 | return code == 200, nil 117 | } 118 | 119 | func (c *Client) Hash(path ...string) (Hash, error) { 120 | res, _, err := c.Request("GET", "/hash/"+JoinPath(path), nil) 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | return string(res), nil 126 | } 127 | 128 | func (c *Client) Remove(path ...string) (bool, error) { 129 | _, code, err := c.Request("DELETE", "/module/"+JoinPath(path), nil) 130 | if err != nil { 131 | return false, nil 132 | } 133 | 134 | return code == 200, nil 135 | } 136 | 137 | func (c *Client) Snapshot() (CommitHash, error) { 138 | res, _, err := c.Request("GET", "/snapshot", nil) 139 | if err != nil { 140 | return "", err 141 | } 142 | 143 | return string(res), nil 144 | } 145 | 146 | func (c *Client) Restore(hash CommitHash, path ...string) (bool, error) { 147 | _, code, err := c.Request("POST", "/restore/"+hash+"/"+JoinPath(path), nil) 148 | if err != nil { 149 | return false, err 150 | } 151 | 152 | return code == 200, nil 153 | } 154 | 155 | func (c *Client) Rollback(path ...string) (bool, error) { 156 | _, code, err := c.Request("POST", "/rollback/"+JoinPath(path), nil) 157 | if err != nil { 158 | return false, err 159 | } 160 | 161 | return code == 200, nil 162 | } 163 | 164 | func (c *Client) Merge(branch string) (bool, error) { 165 | _, code, err := c.Request("POST", "/merge/"+branch, nil) 166 | if err != nil { 167 | return false, err 168 | } 169 | 170 | return code == 200, nil 171 | } 172 | 173 | func (c *Client) Gc() (bool, error) { 174 | _, code, err := c.Request("POST", "/gc", nil) 175 | if err != nil { 176 | return false, err 177 | } 178 | 179 | return code == 200, nil 180 | } 181 | 182 | func (c *Client) CreateBranch(branch string) (bool, error) { 183 | _, code, err := c.Request("POST", "/branch/"+branch, nil) 184 | if err != nil { 185 | return false, err 186 | } 187 | 188 | return code == 200, nil 189 | } 190 | 191 | func (c *Client) DeleteBranch(branch string) (bool, error) { 192 | _, code, err := c.Request("DELETE", "/branch/"+branch, nil) 193 | if err != nil { 194 | return false, err 195 | } 196 | 197 | return code == 200, nil 198 | } 199 | 200 | func (c *Client) Branches() ([]string, error) { 201 | res, _, err := c.Request("GET", "/branches", nil) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | var s []string 207 | err = json.Unmarshal(res, &s) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | return s, nil 213 | } 214 | 215 | func (c *Client) Versions(path ...string) ([][]Hash, error) { 216 | res, _, err := c.Request("GET", "/versions/"+JoinPath(path), nil) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | var s [][]Hash 222 | err = json.Unmarshal(res, &s) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | return s, nil 228 | } 229 | 230 | func (c *Client) List(path ...string) (map[string]Hash, error) { 231 | res, _, err := c.Request("GET", "/modules/"+JoinPath(path), nil) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | var s map[string]Hash 237 | err = json.Unmarshal(res, &s) 238 | return s, err 239 | } 240 | 241 | func (c *Client) Contains(path ...string) (bool, error) { 242 | _, code, err := c.Request("HEAD", "/module/"+JoinPath(path), nil) 243 | return code == 200, err 244 | } 245 | 246 | func (c *Client) CommitInfo(hash CommitHash) (CommitInfo, error) { 247 | res, _, err := c.Request("GET", "/commit/"+hash, nil) 248 | if err != nil { 249 | return CommitInfo{}, err 250 | } 251 | 252 | var s CommitInfo 253 | err = json.Unmarshal(res, &s) 254 | if err != nil { 255 | return CommitInfo{}, err 256 | } 257 | 258 | return s, nil 259 | } 260 | 261 | func (c *Client) Auth(method string) (bool, error) { 262 | _, code, err := c.Request(method, "/auth", nil) 263 | return code == 200, err 264 | } 265 | -------------------------------------------------------------------------------- /client/go/wasmstore_test.go: -------------------------------------------------------------------------------- 1 | package wasmstore 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestClient(t *testing.T) { 8 | client, err := NewClient("http://127.0.0.1:6384", nil) 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | 13 | modules, err := client.List() 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | 18 | if len(modules) == 0 { 19 | return 20 | } 21 | 22 | for k, v := range modules { 23 | a, ok, err := client.Find(k) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | 28 | if !ok { 29 | t.Error("not ok") 30 | } 31 | 32 | b, ok, err := client.Find(v) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | if !ok { 38 | t.Error("not ok") 39 | } 40 | 41 | if len(a) != len(b) { 42 | t.Error("values have different lengths") 43 | } 44 | for i := range a { 45 | if a[i] != b[i] { 46 | t.Error("values don't match") 47 | } 48 | } 49 | break 50 | } 51 | 52 | hash, err := client.Snapshot() 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | 57 | _, err = client.CommitInfo(hash) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | } -------------------------------------------------------------------------------- /client/js/README.md: -------------------------------------------------------------------------------- 1 | # wasmstore 2 | 3 | A Javascript client for [wasmstore](https://github.com/dylibso/wasmstore) with support for Node and Browser 4 | -------------------------------------------------------------------------------- /client/js/example.js: -------------------------------------------------------------------------------- 1 | import { Client } from "./wasmstore.js"; 2 | 3 | async function main() { 4 | const client = new Client(); 5 | 6 | // List existing modules on the server 7 | const list = await client.list(); 8 | for (const item in list) { 9 | console.log(item); 10 | } 11 | } 12 | 13 | await main(); 14 | -------------------------------------------------------------------------------- /client/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dylibso/wasmstore", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@dylibso/wasmstore", 9 | "version": "0.2.0", 10 | "license": "BSD-3-Clause", 11 | "dependencies": { 12 | "node-fetch": "^3.2.10" 13 | } 14 | }, 15 | "node_modules/data-uri-to-buffer": { 16 | "version": "4.0.1", 17 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 18 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 19 | "engines": { 20 | "node": ">= 12" 21 | } 22 | }, 23 | "node_modules/fetch-blob": { 24 | "version": "3.2.0", 25 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 26 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 27 | "funding": [ 28 | { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/jimmywarting" 31 | }, 32 | { 33 | "type": "paypal", 34 | "url": "https://paypal.me/jimmywarting" 35 | } 36 | ], 37 | "dependencies": { 38 | "node-domexception": "^1.0.0", 39 | "web-streams-polyfill": "^3.0.3" 40 | }, 41 | "engines": { 42 | "node": "^12.20 || >= 14.13" 43 | } 44 | }, 45 | "node_modules/formdata-polyfill": { 46 | "version": "4.0.10", 47 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 48 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 49 | "dependencies": { 50 | "fetch-blob": "^3.1.2" 51 | }, 52 | "engines": { 53 | "node": ">=12.20.0" 54 | } 55 | }, 56 | "node_modules/node-domexception": { 57 | "version": "1.0.0", 58 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 59 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 60 | "funding": [ 61 | { 62 | "type": "github", 63 | "url": "https://github.com/sponsors/jimmywarting" 64 | }, 65 | { 66 | "type": "github", 67 | "url": "https://paypal.me/jimmywarting" 68 | } 69 | ], 70 | "engines": { 71 | "node": ">=10.5.0" 72 | } 73 | }, 74 | "node_modules/node-fetch": { 75 | "version": "3.3.2", 76 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 77 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 78 | "dependencies": { 79 | "data-uri-to-buffer": "^4.0.0", 80 | "fetch-blob": "^3.1.4", 81 | "formdata-polyfill": "^4.0.10" 82 | }, 83 | "engines": { 84 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 85 | }, 86 | "funding": { 87 | "type": "opencollective", 88 | "url": "https://opencollective.com/node-fetch" 89 | } 90 | }, 91 | "node_modules/web-streams-polyfill": { 92 | "version": "3.3.3", 93 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 94 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 95 | "engines": { 96 | "node": ">= 8" 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dylibso/wasmstore", 3 | "version": "0.3.0", 4 | "description": "Wasmstore client", 5 | "main": "wasmstore.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "BSD-3-Clause", 11 | "type": "module", 12 | "dependencies": { 13 | "node-fetch": "^3.2.10" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/js/wasmstore.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | try { 3 | const node_fetch = await import("node-fetch"); 4 | global.fetch = node_fetch.default; 5 | } catch (_) { 6 | } 7 | })().catch((_) => { 8 | return; 9 | }); 10 | 11 | export function request( 12 | method, 13 | url, 14 | body = null, 15 | auth = null, 16 | branch = null, 17 | ) { 18 | const opts = { 19 | method, 20 | mode: "cors", 21 | headers: {}, 22 | }; 23 | 24 | if (body !== null) { 25 | opts.body = body; 26 | } 27 | 28 | if (auth !== null) { 29 | opts.headers["Wasmstore-Auth"] = auth; 30 | } 31 | 32 | if (branch !== null) { 33 | opts.headers["Wasmstore-Branch"] = branch; 34 | } 35 | 36 | return fetch(url, opts); 37 | } 38 | 39 | function normalizePath(path) { 40 | if (typeof path === "string") { 41 | return path; 42 | } 43 | 44 | return path.join("/"); 45 | } 46 | 47 | function pathString(path) { 48 | if (path === null) { 49 | return ""; 50 | } 51 | return normalizePath(path); 52 | } 53 | 54 | export class Client { 55 | constructor( 56 | url = "http://127.0.0.1:6384", 57 | branch = null, 58 | auth = null, 59 | version = "v1", 60 | ) { 61 | this.url = url + "/api/" + version; 62 | this.auth = auth; 63 | this.branch = branch; 64 | } 65 | 66 | request(method, url, body = null) { 67 | return request(method, this.url + url, body, this.auth, this.branch); 68 | } 69 | 70 | async find(path) { 71 | const res = await this.request("GET", "/module/" + pathString(path)); 72 | if (res.status === 404) { 73 | return null; 74 | } 75 | 76 | return res; 77 | } 78 | 79 | async instantiate(path, imports = { env: {} }) { 80 | const res = await this.find(path); 81 | return WebAssembly.instantiateStreaming(res, imports); 82 | } 83 | 84 | async hash(path) { 85 | const res = await this.request("GET", "/hash/" + pathString(path)); 86 | if (res.status === 404) { 87 | return null; 88 | } 89 | 90 | return await res.text(); 91 | } 92 | 93 | async add(path, data) { 94 | const res = await this.request("POST", "/module/" + pathString(path), data); 95 | return await res.text(); 96 | } 97 | 98 | async snapshot() { 99 | const res = await this.request("GET", "/snapshot"); 100 | return await res.text(); 101 | } 102 | 103 | async restore(hash, path = null) { 104 | const res = await this.request( 105 | "POST", 106 | "/restore/" + hash + (path === null ? "" : "/" + pathString(path)), 107 | ); 108 | return res.ok; 109 | } 110 | 111 | async rollback(path = null) { 112 | const res = await this.request("POST", "/rollback/" + pathString(path)); 113 | return res.ok; 114 | } 115 | 116 | async gc() { 117 | const res = await this.request("POST", "/gc"); 118 | return res.ok; 119 | } 120 | 121 | async versions(path) { 122 | const res = await this.request("GET", "/versions/" + pathString(path)); 123 | return await res.json(); 124 | } 125 | 126 | async list(path = null) { 127 | const res = await this.request("GET", "/modules/" + pathString(path)); 128 | return await res.json(); 129 | } 130 | 131 | async branches() { 132 | const res = await this.request("GET", "/branches"); 133 | return await res.json(); 134 | } 135 | 136 | async createBranch(name) { 137 | const res = await this.request("POST", "/branch/" + name); 138 | return res.ok; 139 | } 140 | 141 | async deleteBranch(name) { 142 | const res = await this.request("DELETE", "/branch/" + name); 143 | return res.ok; 144 | } 145 | 146 | async set(path, hash) { 147 | const res = await this.request( 148 | "POST", 149 | "/hash/" + hash + "/" + pathString(path), 150 | ); 151 | return res.ok; 152 | } 153 | 154 | async delete(path) { 155 | const res = await this.request("DELETE", "/module/" + pathString(path)); 156 | return res.ok; 157 | } 158 | 159 | async contains(path) { 160 | const res = await this.request("HEAD", "/module/" + pathString(path)); 161 | return res.ok; 162 | } 163 | 164 | async commitInfo(hash) { 165 | const res = await this.request("GET", "/commit/" + hash); 166 | return await res.json(); 167 | } 168 | 169 | watch(callback) { 170 | const ws = new WebSocket(this.url.replace("http", "ws") + "/watch"); 171 | 172 | ws.onmessage = function (msg) { 173 | callback(JSON.parse(msg.data)); 174 | }; 175 | 176 | return ws; 177 | } 178 | 179 | async auth(method) { 180 | const res = await this.request(method, "/auth"); 181 | return res.ok; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /client/python/README.md: -------------------------------------------------------------------------------- 1 | # wasmstore 2 | 3 | A Python client for [wasmstore](https://github.com/dylibso/wasmstore) 4 | -------------------------------------------------------------------------------- /client/python/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.8.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, 11 | {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, 12 | {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, 13 | {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, 14 | {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, 15 | {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, 16 | {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, 17 | {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, 18 | {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, 19 | {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, 20 | {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, 21 | {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, 22 | {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, 23 | {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, 24 | {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, 25 | {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, 26 | {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, 27 | {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, 28 | {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, 29 | {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, 30 | {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, 31 | {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, 32 | ] 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | packaging = ">=22.0" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 41 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 42 | 43 | [package.extras] 44 | colorama = ["colorama (>=0.4.3)"] 45 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 46 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 47 | uvloop = ["uvloop (>=0.15.2)"] 48 | 49 | [[package]] 50 | name = "certifi" 51 | version = "2024.8.30" 52 | description = "Python package for providing Mozilla's CA Bundle." 53 | optional = false 54 | python-versions = ">=3.6" 55 | files = [ 56 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 57 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 58 | ] 59 | 60 | [[package]] 61 | name = "charset-normalizer" 62 | version = "3.3.2" 63 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 64 | optional = false 65 | python-versions = ">=3.7.0" 66 | files = [ 67 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 68 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 69 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 70 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 71 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 72 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 73 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 74 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 75 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 76 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 77 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 78 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 79 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 80 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 81 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 82 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 83 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 84 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 85 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 86 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 87 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 88 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 89 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 90 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 91 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 92 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 93 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 94 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 95 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 96 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 97 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 98 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 99 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 100 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 101 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 102 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 103 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 104 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 105 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 106 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 107 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 108 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 109 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 110 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 111 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 112 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 113 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 114 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 115 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 116 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 117 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 118 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 119 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 120 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 121 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 122 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 123 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 124 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 125 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 126 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 127 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 128 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 129 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 130 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 131 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 132 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 133 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 134 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 135 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 136 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 137 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 138 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 139 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 140 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 141 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 142 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 143 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 144 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 145 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 146 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 147 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 148 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 149 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 150 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 151 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 152 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 153 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 154 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 155 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 156 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 157 | ] 158 | 159 | [[package]] 160 | name = "click" 161 | version = "8.1.7" 162 | description = "Composable command line interface toolkit" 163 | optional = false 164 | python-versions = ">=3.7" 165 | files = [ 166 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 167 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 168 | ] 169 | 170 | [package.dependencies] 171 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 172 | 173 | [[package]] 174 | name = "colorama" 175 | version = "0.4.6" 176 | description = "Cross-platform colored terminal text." 177 | optional = false 178 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 179 | files = [ 180 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 181 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 182 | ] 183 | 184 | [[package]] 185 | name = "docstring-to-markdown" 186 | version = "0.15" 187 | description = "On the fly conversion of Python docstrings to markdown" 188 | optional = false 189 | python-versions = ">=3.6" 190 | files = [ 191 | {file = "docstring-to-markdown-0.15.tar.gz", hash = "sha256:e146114d9c50c181b1d25505054a8d0f7a476837f0da2c19f07e06eaed52b73d"}, 192 | {file = "docstring_to_markdown-0.15-py3-none-any.whl", hash = "sha256:27afb3faedba81e34c33521c32bbd258d7fbb79eedf7d29bc4e81080e854aec0"}, 193 | ] 194 | 195 | [[package]] 196 | name = "exceptiongroup" 197 | version = "1.2.2" 198 | description = "Backport of PEP 654 (exception groups)" 199 | optional = false 200 | python-versions = ">=3.7" 201 | files = [ 202 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 203 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 204 | ] 205 | 206 | [package.extras] 207 | test = ["pytest (>=6)"] 208 | 209 | [[package]] 210 | name = "idna" 211 | version = "3.10" 212 | description = "Internationalized Domain Names in Applications (IDNA)" 213 | optional = false 214 | python-versions = ">=3.6" 215 | files = [ 216 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 217 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 218 | ] 219 | 220 | [package.extras] 221 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 222 | 223 | [[package]] 224 | name = "importlib-metadata" 225 | version = "8.5.0" 226 | description = "Read metadata from Python packages" 227 | optional = false 228 | python-versions = ">=3.8" 229 | files = [ 230 | {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, 231 | {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, 232 | ] 233 | 234 | [package.dependencies] 235 | zipp = ">=3.20" 236 | 237 | [package.extras] 238 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 239 | cover = ["pytest-cov"] 240 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 241 | enabler = ["pytest-enabler (>=2.2)"] 242 | perf = ["ipython"] 243 | test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] 244 | type = ["pytest-mypy"] 245 | 246 | [[package]] 247 | name = "iniconfig" 248 | version = "2.0.0" 249 | description = "brain-dead simple config-ini parsing" 250 | optional = false 251 | python-versions = ">=3.7" 252 | files = [ 253 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 254 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 255 | ] 256 | 257 | [[package]] 258 | name = "jedi" 259 | version = "0.19.1" 260 | description = "An autocompletion tool for Python that can be used for text editors." 261 | optional = false 262 | python-versions = ">=3.6" 263 | files = [ 264 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 265 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 266 | ] 267 | 268 | [package.dependencies] 269 | parso = ">=0.8.3,<0.9.0" 270 | 271 | [package.extras] 272 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 273 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 274 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 275 | 276 | [[package]] 277 | name = "mypy-extensions" 278 | version = "1.0.0" 279 | description = "Type system extensions for programs checked with the mypy type checker." 280 | optional = false 281 | python-versions = ">=3.5" 282 | files = [ 283 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 284 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 285 | ] 286 | 287 | [[package]] 288 | name = "packaging" 289 | version = "24.1" 290 | description = "Core utilities for Python packages" 291 | optional = false 292 | python-versions = ">=3.8" 293 | files = [ 294 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 295 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 296 | ] 297 | 298 | [[package]] 299 | name = "parso" 300 | version = "0.8.4" 301 | description = "A Python Parser" 302 | optional = false 303 | python-versions = ">=3.6" 304 | files = [ 305 | {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, 306 | {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, 307 | ] 308 | 309 | [package.extras] 310 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 311 | testing = ["docopt", "pytest"] 312 | 313 | [[package]] 314 | name = "pathspec" 315 | version = "0.12.1" 316 | description = "Utility library for gitignore style pattern matching of file paths." 317 | optional = false 318 | python-versions = ">=3.8" 319 | files = [ 320 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 321 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 322 | ] 323 | 324 | [[package]] 325 | name = "platformdirs" 326 | version = "4.3.6" 327 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 328 | optional = false 329 | python-versions = ">=3.8" 330 | files = [ 331 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 332 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 333 | ] 334 | 335 | [package.extras] 336 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 337 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 338 | type = ["mypy (>=1.11.2)"] 339 | 340 | [[package]] 341 | name = "pluggy" 342 | version = "1.5.0" 343 | description = "plugin and hook calling mechanisms for python" 344 | optional = false 345 | python-versions = ">=3.8" 346 | files = [ 347 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 348 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 349 | ] 350 | 351 | [package.extras] 352 | dev = ["pre-commit", "tox"] 353 | testing = ["pytest", "pytest-benchmark"] 354 | 355 | [[package]] 356 | name = "pytest" 357 | version = "7.4.4" 358 | description = "pytest: simple powerful testing with Python" 359 | optional = false 360 | python-versions = ">=3.7" 361 | files = [ 362 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 363 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 364 | ] 365 | 366 | [package.dependencies] 367 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 368 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 369 | iniconfig = "*" 370 | packaging = "*" 371 | pluggy = ">=0.12,<2.0" 372 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 373 | 374 | [package.extras] 375 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 376 | 377 | [[package]] 378 | name = "python-lsp-jsonrpc" 379 | version = "1.1.2" 380 | description = "JSON RPC 2.0 server library" 381 | optional = false 382 | python-versions = ">=3.8" 383 | files = [ 384 | {file = "python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912"}, 385 | {file = "python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c"}, 386 | ] 387 | 388 | [package.dependencies] 389 | ujson = ">=3.0.0" 390 | 391 | [package.extras] 392 | test = ["coverage", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-cov"] 393 | 394 | [[package]] 395 | name = "python-lsp-server" 396 | version = "1.12.0" 397 | description = "Python Language Server for the Language Server Protocol" 398 | optional = false 399 | python-versions = ">=3.8" 400 | files = [ 401 | {file = "python_lsp_server-1.12.0-py3-none-any.whl", hash = "sha256:2e912c661881d85f67f2076e4e66268b695b62bf127e07e81f58b187d4bb6eda"}, 402 | {file = "python_lsp_server-1.12.0.tar.gz", hash = "sha256:b6a336f128da03bd9bac1e61c3acca6e84242b8b31055a1ccf49d83df9dc053b"}, 403 | ] 404 | 405 | [package.dependencies] 406 | docstring-to-markdown = "*" 407 | importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} 408 | jedi = ">=0.17.2,<0.20.0" 409 | pluggy = ">=1.0.0" 410 | python-lsp-jsonrpc = ">=1.1.0,<2.0.0" 411 | ujson = ">=3.0.0" 412 | 413 | [package.extras] 414 | all = ["autopep8 (>=2.0.4,<2.1.0)", "flake8 (>=7.1,<8)", "mccabe (>=0.7.0,<0.8.0)", "pycodestyle (>=2.12.0,<2.13.0)", "pydocstyle (>=6.3.0,<6.4.0)", "pyflakes (>=3.2.0,<3.3.0)", "pylint (>=3.1,<4)", "rope (>=1.11.0)", "whatthepatch (>=1.0.2,<2.0.0)", "yapf (>=0.33.0)"] 415 | autopep8 = ["autopep8 (>=2.0.4,<2.1.0)"] 416 | flake8 = ["flake8 (>=7.1,<8)"] 417 | mccabe = ["mccabe (>=0.7.0,<0.8.0)"] 418 | pycodestyle = ["pycodestyle (>=2.12.0,<2.13.0)"] 419 | pydocstyle = ["pydocstyle (>=6.3.0,<6.4.0)"] 420 | pyflakes = ["pyflakes (>=3.2.0,<3.3.0)"] 421 | pylint = ["pylint (>=3.1,<4)"] 422 | rope = ["rope (>=1.11.0)"] 423 | test = ["coverage", "flaky", "matplotlib", "numpy", "pandas", "pylint (>=3.1,<4)", "pyqt5", "pytest", "pytest-cov"] 424 | websockets = ["websockets (>=10.3)"] 425 | yapf = ["whatthepatch (>=1.0.2,<2.0.0)", "yapf (>=0.33.0)"] 426 | 427 | [[package]] 428 | name = "requests" 429 | version = "2.32.3" 430 | description = "Python HTTP for Humans." 431 | optional = false 432 | python-versions = ">=3.8" 433 | files = [ 434 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 435 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 436 | ] 437 | 438 | [package.dependencies] 439 | certifi = ">=2017.4.17" 440 | charset-normalizer = ">=2,<4" 441 | idna = ">=2.5,<4" 442 | urllib3 = ">=1.21.1,<3" 443 | 444 | [package.extras] 445 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 446 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 447 | 448 | [[package]] 449 | name = "tomli" 450 | version = "2.0.1" 451 | description = "A lil' TOML parser" 452 | optional = false 453 | python-versions = ">=3.7" 454 | files = [ 455 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 456 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 457 | ] 458 | 459 | [[package]] 460 | name = "typing-extensions" 461 | version = "4.12.2" 462 | description = "Backported and Experimental Type Hints for Python 3.8+" 463 | optional = false 464 | python-versions = ">=3.8" 465 | files = [ 466 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 467 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 468 | ] 469 | 470 | [[package]] 471 | name = "ujson" 472 | version = "5.10.0" 473 | description = "Ultra fast JSON encoder and decoder for Python" 474 | optional = false 475 | python-versions = ">=3.8" 476 | files = [ 477 | {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, 478 | {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, 479 | {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, 480 | {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, 481 | {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, 482 | {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, 483 | {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, 484 | {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, 485 | {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, 486 | {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, 487 | {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, 488 | {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, 489 | {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, 490 | {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, 491 | {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, 492 | {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, 493 | {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, 494 | {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, 495 | {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, 496 | {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, 497 | {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, 498 | {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, 499 | {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, 500 | {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, 501 | {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, 502 | {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, 503 | {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, 504 | {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, 505 | {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, 506 | {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, 507 | {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, 508 | {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, 509 | {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, 510 | {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, 511 | {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, 512 | {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, 513 | {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, 514 | {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, 515 | {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, 516 | {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, 517 | {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, 518 | {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, 519 | {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, 520 | {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, 521 | {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, 522 | {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, 523 | {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, 524 | {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, 525 | {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, 526 | {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, 527 | {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, 528 | {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, 529 | {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, 530 | {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, 531 | {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, 532 | {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, 533 | {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, 534 | {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, 535 | {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, 536 | {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, 537 | {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, 538 | {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, 539 | {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, 540 | {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, 541 | {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, 542 | {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, 543 | {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, 544 | {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, 545 | {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, 546 | {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, 547 | {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, 548 | {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, 549 | {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, 550 | {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, 551 | {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, 552 | {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, 553 | {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, 554 | {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, 555 | ] 556 | 557 | [[package]] 558 | name = "urllib3" 559 | version = "2.2.3" 560 | description = "HTTP library with thread-safe connection pooling, file post, and more." 561 | optional = false 562 | python-versions = ">=3.8" 563 | files = [ 564 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 565 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 566 | ] 567 | 568 | [package.extras] 569 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 570 | h2 = ["h2 (>=4,<5)"] 571 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 572 | zstd = ["zstandard (>=0.18.0)"] 573 | 574 | [[package]] 575 | name = "zipp" 576 | version = "3.20.2" 577 | description = "Backport of pathlib-compatible object wrapper for zip files" 578 | optional = false 579 | python-versions = ">=3.8" 580 | files = [ 581 | {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, 582 | {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, 583 | ] 584 | 585 | [package.extras] 586 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 587 | cover = ["pytest-cov"] 588 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 589 | enabler = ["pytest-enabler (>=2.2)"] 590 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 591 | type = ["pytest-mypy"] 592 | 593 | [metadata] 594 | lock-version = "2.0" 595 | python-versions = "^3.9" 596 | content-hash = "d521b0b307cb24cfa2b415ca865cd06e343232f2b67b952c0f1d7bf080a58c32" 597 | -------------------------------------------------------------------------------- /client/python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wasmstore" 3 | version = "0.3.0" 4 | description = "Wasmstore client for Python" 5 | authors = ["Dylibso "] 6 | license = "BSD-3-Clause" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | requests = "^2.28.1" 12 | 13 | [tool.poetry.dev-dependencies] 14 | black = "^24.03.0" 15 | python-lsp-server = "^1.5.0" 16 | pytest = "^7.1.3" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /client/python/test/test_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.insert(0, ".") 4 | 5 | from wasmstore import Client 6 | 7 | def test_client(): 8 | client = Client() 9 | modules = client.list() 10 | if len(modules) > 0: 11 | first = list(modules.keys())[0] 12 | assert (client.find(first) is not None) -------------------------------------------------------------------------------- /client/python/wasmstore/__init__.py: -------------------------------------------------------------------------------- 1 | from .wasmstore import Client, Error 2 | -------------------------------------------------------------------------------- /client/python/wasmstore/wasmstore.py: -------------------------------------------------------------------------------- 1 | from requests import request 2 | 3 | 4 | class Error(Exception): 5 | pass 6 | 7 | 8 | def normalize_path(path): 9 | if isinstance(path, list): 10 | return "/".join(path) 11 | elif isinstance(path, str): 12 | return path.lstrip("/") 13 | else: 14 | raise Error("invalid path") 15 | 16 | 17 | class Client: 18 | 19 | def __init__( 20 | self, url="http://127.0.0.1:6384", version="v1", auth=None, branch=None 21 | ): 22 | self.url = url + "/api/" + version 23 | self.auth = auth 24 | self.branch = branch 25 | 26 | def request(self, method, route, body=None): 27 | headers = {} 28 | if self.auth is not None: 29 | headers["Wasmstore-Auth"] = self.auth 30 | if self.branch is not None: 31 | headers["Wasmstore-Branch"] = self.branch 32 | return request(method, self.url + route, headers=headers, data=body) 33 | 34 | def find(self, path): 35 | res = self.request("GET", "/module/" + normalize_path(path)) 36 | if res.status_code == 404: 37 | return None 38 | return res.content 39 | 40 | def add(self, path, data): 41 | res = self.request("POST", "/module/" + normalize_path(path), body=data) 42 | return res.text 43 | 44 | def set(self, path, hash): 45 | res = self.request("POST", "/hash/" + hash + "/" + normalize_pathr(path)) 46 | return res.text 47 | 48 | def hash(self, path): 49 | res = self.request("GET", "/hash/" + normalize_path(path)) 50 | if not res.ok: 51 | return None 52 | return res.text 53 | 54 | def remove(self, path): 55 | path = normalize_path(path) 56 | res = self.request("DELETE", "/module/" + path) 57 | return res.ok 58 | 59 | def snapshot(self): 60 | res = self.request("GET", "/snapshot") 61 | if not res.ok: 62 | return None 63 | return res.text 64 | 65 | def restore(self, hash, path=None): 66 | url = "/restore/" + hash 67 | if path is not None: 68 | url += "/" 69 | url += normalize_path(path) 70 | res = self.request("POST", url) 71 | return res.ok 72 | 73 | def rollback(self, path=None): 74 | url = "/rollback" 75 | if path is not None: 76 | url += "/" 77 | url += normalize_path(path) 78 | res = self.request("POST", url) 79 | return res.ok 80 | 81 | def versions(self, path): 82 | res = self.request("GET", "/versions/" + normalize_path(path)) 83 | return res.json() 84 | 85 | def list(self, path=None): 86 | url = "/modules" 87 | if path is not None: 88 | url += "/" + normalize_path(path) 89 | res = self.request("GET", url) 90 | return res.json() 91 | 92 | def branches(self): 93 | res = self.request("GET", "/branches") 94 | return res.json() 95 | 96 | def gc(self): 97 | res = self.request("POST", "/gc") 98 | return res.ok 99 | 100 | def create_branch(self, name): 101 | res = self.request("POST", "/branch/" + name) 102 | return res.ok 103 | 104 | def delete_branch(self, name): 105 | res = self.request("DELETE", "/branch/" + name) 106 | return res.ok 107 | 108 | def merge(self, branch): 109 | res = self.request("POST", "/merge/" + branch) 110 | return res.ok 111 | 112 | def contains(self, path): 113 | res = self.request("HEAD", "/module/" + normalize_path(path)) 114 | return res.ok 115 | 116 | def commit_info(self, hash): 117 | res = self.request("GET", "/commit/" + hash) 118 | return res.json() 119 | 120 | def auth(self, method): 121 | res = self.request(method, "/auth") 122 | return res.ok 123 | -------------------------------------------------------------------------------- /client/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmstore-client" 3 | version = "0.3.0" 4 | edition = "2021" 5 | description = "Wasmstore client" 6 | authors = ["Dylibso Inc", "oss@dylibso.com"] 7 | license = "BSD-3-Clause" 8 | repository = "https://github.com/dylibso/wasmstore" 9 | 10 | [dependencies] 11 | reqwest = {version = "0.12", features = ["json"]} 12 | anyhow = "1" 13 | tokio = {version = "1", features = ["full"]} 14 | serde = {version = "1", features = ["derive"]} 15 | 16 | [workspace] 17 | -------------------------------------------------------------------------------- /client/rust/README.md: -------------------------------------------------------------------------------- 1 | # wasmstore 2 | 3 | A Rust client for [wasmstore](https://github.com/dylibso/wasmstore) 4 | -------------------------------------------------------------------------------- /client/rust/src/hash.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] 2 | #[serde(transparent)] 3 | pub struct Hash(pub String); 4 | 5 | impl From for Hash { 6 | fn from(s: String) -> Self { 7 | Hash(s) 8 | } 9 | } 10 | 11 | impl From for String { 12 | fn from(h: Hash) -> Self { 13 | h.0 14 | } 15 | } 16 | 17 | impl AsRef for Hash { 18 | fn as_ref(&self) -> &str { 19 | self.0.as_str() 20 | } 21 | } 22 | 23 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] 24 | #[serde(transparent)] 25 | pub struct Commit(pub String); 26 | 27 | impl From for Commit { 28 | fn from(s: String) -> Self { 29 | Commit(s) 30 | } 31 | } 32 | 33 | impl From for String { 34 | fn from(h: Commit) -> Self { 35 | h.0 36 | } 37 | } 38 | 39 | impl AsRef for Commit { 40 | fn as_ref(&self) -> &str { 41 | self.0.as_str() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::Error; 2 | 3 | mod hash; 4 | mod path; 5 | 6 | pub use hash::{Commit, Hash}; 7 | pub use path::Path; 8 | 9 | #[derive(Clone)] 10 | pub struct Client { 11 | url: reqwest::Url, 12 | client: reqwest::Client, 13 | version: Version, 14 | auth: Option, 15 | branch: Option, 16 | } 17 | 18 | #[derive(serde::Deserialize)] 19 | pub struct CommitInfo { 20 | pub hash: Commit, 21 | pub parents: Option>, 22 | pub date: i64, 23 | pub author: String, 24 | pub message: String, 25 | } 26 | 27 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 | pub enum Version { 29 | V1, 30 | } 31 | 32 | impl Client { 33 | pub fn new(url: impl reqwest::IntoUrl, version: Version) -> Result { 34 | let url = url.into_url()?; 35 | 36 | let client = reqwest::Client::new(); 37 | 38 | Ok(Client { 39 | url, 40 | client, 41 | version, 42 | auth: None, 43 | branch: None, 44 | }) 45 | } 46 | 47 | pub fn with_auth(mut self, auth: String) -> Client { 48 | self.auth = Some(auth); 49 | self 50 | } 51 | 52 | pub fn with_branch(mut self, branch: String) -> Client { 53 | self.branch = Some(branch); 54 | self 55 | } 56 | 57 | pub async fn request( 58 | &self, 59 | method: reqwest::Method, 60 | endpoint: impl AsRef, 61 | body: Option>, 62 | ) -> Result { 63 | let url = self.url.join(endpoint.as_ref())?; 64 | let mut builder = self.client.request(method, url); 65 | 66 | if let Some(auth) = &self.auth { 67 | builder = builder.header("Wasmstore-Auth", auth); 68 | } 69 | 70 | if let Some(branch) = &self.branch { 71 | builder = builder.header("Wasmstore-Branch", branch); 72 | } 73 | 74 | if let Some(body) = body { 75 | builder = builder.body(body); 76 | } 77 | 78 | let res = builder.send().await?; 79 | Ok(res) 80 | } 81 | 82 | fn endpoint(&self, endpoint: &str) -> String { 83 | let v = match self.version { 84 | Version::V1 => "v1", 85 | }; 86 | format!("/api/{v}{endpoint}") 87 | } 88 | 89 | pub async fn find(&self, path: impl Into) -> Result, Hash)>, Error> { 90 | let path = path.into(); 91 | let p = format!("/module/{}", path.to_string()); 92 | let res = self 93 | .request(reqwest::Method::GET, self.endpoint(&p), None) 94 | .await?; 95 | if res.status() == reqwest::StatusCode::NOT_FOUND { 96 | return Ok(None); 97 | } 98 | 99 | if !res.status().is_success() { 100 | return Err(Error::msg(res.text().await?)); 101 | } 102 | let hash = res 103 | .headers() 104 | .get("Wasmstore-Hash") 105 | .expect("Wasmstore-Hash header is unset in find response") 106 | .to_str()? 107 | .to_string(); 108 | let b = res.bytes().await?; 109 | Ok(Some((b.to_vec(), Hash(hash)))) 110 | } 111 | 112 | pub async fn hash(&self, path: impl Into) -> Result, Error> { 113 | let path = path.into(); 114 | let p = format!("/hash/{}", path.to_string()); 115 | let res = self 116 | .request(reqwest::Method::GET, self.endpoint(&p), None) 117 | .await?; 118 | if res.status() == reqwest::StatusCode::NOT_FOUND { 119 | return Ok(None); 120 | } 121 | 122 | if !res.status().is_success() { 123 | return Err(Error::msg(res.text().await?)); 124 | } 125 | 126 | let b = res.text().await?; 127 | Ok(Some(Hash(b))) 128 | } 129 | 130 | pub async fn add(&self, path: impl Into, data: Vec) -> Result { 131 | let path = path.into(); 132 | let p = format!("/module/{}", path.to_string()); 133 | let res = self 134 | .request(reqwest::Method::POST, self.endpoint(&p), Some(data)) 135 | .await?; 136 | 137 | if !res.status().is_success() { 138 | return Err(Error::msg(res.text().await?)); 139 | } 140 | 141 | let b = res.text().await?; 142 | Ok(Hash(b)) 143 | } 144 | 145 | pub async fn remove(&self, path: impl Into) -> Result<(), Error> { 146 | let path = path.into(); 147 | let p = format!("/module/{}", path.to_string()); 148 | let res = self 149 | .request(reqwest::Method::DELETE, self.endpoint(&p), None) 150 | .await?; 151 | if !res.status().is_success() { 152 | return Err(Error::msg(res.text().await?)); 153 | } 154 | Ok(()) 155 | } 156 | 157 | pub async fn gc(&self) -> Result<(), Error> { 158 | let res = self 159 | .request(reqwest::Method::POST, self.endpoint("/gc"), None) 160 | .await?; 161 | if !res.status().is_success() { 162 | return Err(Error::msg(res.text().await?)); 163 | } 164 | Ok(()) 165 | } 166 | 167 | pub async fn list( 168 | &self, 169 | path: impl Into, 170 | ) -> Result, Error> { 171 | let path = path.into(); 172 | let p = format!("/modules/{}", path.to_string()); 173 | let res = self 174 | .request(reqwest::Method::GET, self.endpoint(&p), None) 175 | .await?; 176 | if !res.status().is_success() { 177 | return Err(Error::msg(res.text().await?)); 178 | } 179 | let res: std::collections::BTreeMap = res.json().await?; 180 | Ok(res 181 | .into_iter() 182 | .map(|(k, v)| (Path::String(k), Hash(v))) 183 | .collect()) 184 | } 185 | 186 | pub async fn versions(&self, path: impl Into) -> Result, Error> { 187 | let url = format!("/versions/{}", path.into().to_string()); 188 | let res = self 189 | .request(reqwest::Method::GET, self.endpoint(&url), None) 190 | .await?; 191 | if !res.status().is_success() { 192 | return Err(Error::msg(res.text().await?)); 193 | } 194 | Ok(res.json().await?) 195 | } 196 | 197 | pub async fn branches(&self) -> Result, Error> { 198 | let res = self 199 | .request(reqwest::Method::GET, self.endpoint("/branches"), None) 200 | .await?; 201 | if !res.status().is_success() { 202 | return Err(Error::msg(res.text().await?)); 203 | } 204 | Ok(res.json().await?) 205 | } 206 | 207 | pub async fn create_branch(&self, name: impl AsRef) -> Result<(), Error> { 208 | let p = format!("/branch/{}", name.as_ref()); 209 | let res = self 210 | .request(reqwest::Method::POST, self.endpoint(&p), None) 211 | .await?; 212 | if !res.status().is_success() { 213 | return Err(Error::msg(res.text().await?)); 214 | } 215 | Ok(()) 216 | } 217 | 218 | pub async fn delete_branch(&self, name: impl AsRef) -> Result<(), Error> { 219 | let p = format!("/branch/{}", name.as_ref()); 220 | let res = self 221 | .request(reqwest::Method::DELETE, self.endpoint(&p), None) 222 | .await?; 223 | if !res.status().is_success() { 224 | return Err(Error::msg(res.text().await?)); 225 | } 226 | Ok(()) 227 | } 228 | 229 | pub async fn snapshot(&self) -> Result { 230 | let res = self 231 | .request(reqwest::Method::GET, self.endpoint("/snapshot"), None) 232 | .await?; 233 | if !res.status().is_success() { 234 | return Err(Error::msg(res.text().await?)); 235 | } 236 | let hash = res.text().await?; 237 | Ok(Commit(hash)) 238 | } 239 | 240 | pub async fn restore(&self, hash: &Commit) -> Result<(), Error> { 241 | let res = self 242 | .request( 243 | reqwest::Method::POST, 244 | self.endpoint(&format!("/restore/{}", hash.0)), 245 | None, 246 | ) 247 | .await?; 248 | if !res.status().is_success() { 249 | return Err(Error::msg(res.text().await?)); 250 | } 251 | Ok(()) 252 | } 253 | 254 | pub async fn restore_path(&self, hash: &Commit, path: impl Into) -> Result<(), Error> { 255 | let res = self 256 | .request( 257 | reqwest::Method::POST, 258 | self.endpoint(&format!("/restore/{}/{}", hash.0, path.into().to_string())), 259 | None, 260 | ) 261 | .await?; 262 | if !res.status().is_success() { 263 | return Err(Error::msg(res.text().await?)); 264 | } 265 | Ok(()) 266 | } 267 | 268 | pub async fn rollback(&self, path: impl Into) -> Result<(), Error> { 269 | let res = self 270 | .request( 271 | reqwest::Method::POST, 272 | self.endpoint(&format!("/rollback/{}", path.into().to_string())), 273 | None, 274 | ) 275 | .await?; 276 | if !res.status().is_success() { 277 | return Err(Error::msg(res.text().await?)); 278 | } 279 | Ok(()) 280 | } 281 | 282 | pub async fn contains(&self, path: impl Into) -> Result { 283 | let res = self 284 | .request( 285 | reqwest::Method::HEAD, 286 | self.endpoint(&format!("/module/{}", path.into().to_string())), 287 | None, 288 | ) 289 | .await?; 290 | Ok(res.status().is_success()) 291 | } 292 | 293 | pub async fn set(&self, path: impl Into, hash: &Hash) -> Result { 294 | let path = path.into(); 295 | let p = format!("/hash/{}/{}", hash.0, path.to_string()); 296 | let res = self 297 | .request(reqwest::Method::POST, self.endpoint(&p), None) 298 | .await?; 299 | 300 | if !res.status().is_success() { 301 | return Err(Error::msg(res.text().await?)); 302 | } 303 | 304 | let b = res.text().await?; 305 | Ok(Hash(b)) 306 | } 307 | 308 | pub async fn commit_info(&self, commit: &Commit) -> Result { 309 | let p = format!("/commit/{}", commit.0); 310 | let res = self 311 | .request(reqwest::Method::GET, self.endpoint(&p), None) 312 | .await?; 313 | if !res.status().is_success() { 314 | return Err(Error::msg(res.text().await?)); 315 | } 316 | let res: CommitInfo = res.json().await?; 317 | Ok(res) 318 | } 319 | 320 | pub async fn auth(&self, method: reqwest::Method) -> Result { 321 | let res = self.request(method, self.endpoint("/auth"), None).await?; 322 | Ok(res.status() == 200) 323 | } 324 | } 325 | 326 | #[cfg(test)] 327 | mod tests { 328 | use crate::*; 329 | 330 | #[tokio::test] 331 | async fn basic_test() { 332 | let client = Client::new("http://127.0.0.1:6384", Version::V1).unwrap(); 333 | 334 | let data = std::fs::read("../../test/a.wasm").unwrap(); 335 | let hash = client.add("test.wasm", data).await; 336 | println!("HASH: {hash:?}"); 337 | 338 | let data = client.find(hash.unwrap()).await.unwrap(); 339 | let data1 = client.find("test.wasm").await.unwrap(); 340 | 341 | let hash = client.snapshot().await.unwrap(); 342 | client.commit_info(&hash).await.unwrap(); 343 | 344 | client.remove("test.wasm").await.unwrap(); 345 | 346 | client.restore(&hash).await.unwrap(); 347 | 348 | assert!(data.is_some()); 349 | assert!(data1.is_some()); 350 | assert_eq!(data, data1); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /client/rust/src/path.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] 4 | pub enum Path { 5 | String(String), 6 | Vec(Vec), 7 | } 8 | 9 | impl From for Path { 10 | fn from(s: String) -> Self { 11 | Path::String(s) 12 | } 13 | } 14 | 15 | impl From for Path { 16 | fn from(s: Hash) -> Self { 17 | Path::String(s.0) 18 | } 19 | } 20 | 21 | impl From<&Hash> for Path { 22 | fn from(s: &Hash) -> Self { 23 | Path::String(s.0.clone()) 24 | } 25 | } 26 | 27 | impl From> for Path { 28 | fn from(s: Vec) -> Self { 29 | Path::Vec(s) 30 | } 31 | } 32 | 33 | impl From> for Path { 34 | fn from(v: Vec<&str>) -> Self { 35 | Path::Vec(v.iter().map(|x| x.to_string()).collect()) 36 | } 37 | } 38 | 39 | impl From<&[&str]> for Path { 40 | fn from(v: &[&str]) -> Self { 41 | Path::Vec(v.iter().map(|x| x.to_string()).collect()) 42 | } 43 | } 44 | 45 | impl From<&str> for Path { 46 | fn from(v: &str) -> Self { 47 | Path::String(v.to_string()) 48 | } 49 | } 50 | 51 | impl ToString for Path { 52 | fn to_string(&self) -> String { 53 | match self { 54 | Path::String(s) => s.clone(), 55 | Path::Vec(v) => v.join("/"), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | wasmstore: 4 | build: . 5 | ports: 6 | - "6384:6384" 7 | volumes: 8 | - "db:/home/wasmstore/db" 9 | environment: 10 | - WASMSTORE_PORT=6384 11 | volumes: 12 | db: 13 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (dirs :standard \ target) 2 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.2) 2 | (cram enable) 3 | 4 | (name wasmstore) 5 | 6 | (generate_opam_files true) 7 | 8 | (source 9 | (github dylibso/wasmstore)) 10 | 11 | (authors "Dylibso Inc.") 12 | 13 | (maintainers "Zach Shipko ") 14 | 15 | (license BSD-3-clause) 16 | 17 | (documentation https://github.com/dylibso/wasmstore) 18 | 19 | (package 20 | (name wasmstore) 21 | (synopsis "A WASM datastore") 22 | (description "An OCaml library and command line program used to store WebAssembly modules") 23 | (depends 24 | ocaml 25 | dune 26 | irmin 27 | irmin-fs 28 | irmin-watcher 29 | lwt 30 | lwt_eio 31 | eio_main 32 | (cohttp-lwt-unix (>= "6.0.0~alpha2")) 33 | fmt 34 | logs 35 | websocket 36 | (cmdliner (>= "1.0.0")) 37 | yojson 38 | ctypes 39 | ctypes-foreign 40 | conf-rust-2021) 41 | (tags 42 | (topics wasm database irmin))) 43 | 44 | -------------------------------------------------------------------------------- /scripts/cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 4 | -------------------------------------------------------------------------------- /scripts/fill.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for i in $(seq "$1"); do 4 | x=$(date +%s%N) 5 | echo "$x" | wasm-tools smith --min-funcs 10 --min-imports 10 --min-exports 10 | _build/default/bin/main.exe add - "$i/$x.wasm" 6 | done 7 | -------------------------------------------------------------------------------- /scripts/wasmstore.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=wasmstore 3 | 4 | [Service] 5 | ExecStart=/usr/local/bin/wasmstore server 6 | # WorkingDirectory=/ 7 | -------------------------------------------------------------------------------- /src/branch.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Store 3 | 4 | let delete { db; _ } branch = Store.Branch.remove (Store.repo db) branch 5 | 6 | let create t branch = 7 | let* exists = Store.Branch.mem (Store.repo t.db) branch in 8 | if exists then Lwt.return_error (`Msg "Branch already exists") 9 | else 10 | let info = Info.v "Create branch %s" branch in 11 | let* db = Store.of_branch (Store.repo t.db) branch in 12 | let* _ = Store.merge_with_branch db ~info t.branch in 13 | Lwt.return_ok { t with db; branch } 14 | 15 | let switch t branch = 16 | let* exists = Store.Branch.mem (Store.repo t.db) branch in 17 | let* () = 18 | if not exists then 19 | let* _ = create t branch in 20 | Lwt.return_unit 21 | else Lwt.return_unit 22 | in 23 | let+ db = Store.of_branch (Store.repo t.db) branch in 24 | t.branch <- branch; 25 | t.db <- db 26 | 27 | let list t = Store.Branch.list (Store.repo t.db) 28 | -------------------------------------------------------------------------------- /src/diff.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Store 3 | 4 | let update_old_new v v' = 5 | `Assoc 6 | [ 7 | ( "old", 8 | `String (Irmin.Type.to_string Store.Hash.t (Store.Contents.hash v)) ); 9 | ( "new", 10 | `String (Irmin.Type.to_string Store.Hash.t (Store.Contents.hash v')) ); 11 | ] 12 | 13 | let make_diff list = 14 | `Assoc 15 | (List.map 16 | (function 17 | | path, `Updated ((v, _), (v', _)) -> 18 | let path = Irmin.Type.to_string Store.path_t path in 19 | ( path, 20 | `Assoc 21 | [ 22 | ("action", `String "updated"); ("hash", update_old_new v v'); 23 | ] ) 24 | | path, `Removed (v, _) -> 25 | let path = Irmin.Type.to_string Store.path_t path in 26 | ( path, 27 | `Assoc 28 | [ 29 | ("action", `String "added"); 30 | ( "hash", 31 | `String 32 | (Irmin.Type.to_string Store.Hash.t 33 | (Store.Contents.hash v)) ); 34 | ] ) 35 | | path, `Added (v, _) -> 36 | let path = Irmin.Type.to_string Store.path_t path in 37 | ( path, 38 | `Assoc 39 | [ 40 | ("action", `String "added"); 41 | ( "hash", 42 | `String 43 | (Irmin.Type.to_string Store.Hash.t 44 | (Store.Contents.hash v)) ); 45 | ] )) 46 | list) 47 | 48 | let json_of_diff t (diff : Store.commit Irmin.Diff.t) : Yojson.Safe.t Lwt.t = 49 | match diff with 50 | | `Updated (commit, commit') -> 51 | let tree = Store.Commit.tree commit in 52 | let tree' = Store.Commit.tree commit' in 53 | let+ list = Store.Tree.diff tree tree' in 54 | make_diff list 55 | | `Removed commit | `Added commit -> 56 | let tree = Store.Commit.tree commit in 57 | let parents = Store.Commit.parents commit in 58 | let+ changes = 59 | Lwt_list.filter_map_s 60 | (fun parent -> 61 | let* commit = Store.Commit.of_key (repo t) parent in 62 | match commit with 63 | | None -> Lwt.return_none 64 | | Some commit -> 65 | let* x = Store.Tree.diff (Store.Commit.tree commit) tree in 66 | Lwt.return_some x) 67 | parents 68 | in 69 | let changes = List.flatten changes in 70 | make_diff changes 71 | 72 | let string_of_diff t d = 73 | let+ j = json_of_diff t d in 74 | Yojson.Safe.to_string j 75 | 76 | let watch t f = 77 | Store.watch t.db (fun diff -> 78 | let* j = json_of_diff t diff in 79 | f j) 80 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name wasmstore) 3 | (public_name wasmstore) 4 | (libraries 5 | unix 6 | lwt_eio 7 | lwt.unix 8 | irmin.unix 9 | irmin-fs.unix 10 | irmin-watcher 11 | logs.fmt 12 | fmt.tty 13 | fmt.cli 14 | logs.cli 15 | cohttp-lwt-unix 16 | websocket 17 | yojson 18 | ctypes 19 | ctypes.foreign) 20 | (preprocess 21 | (pps ppx_irmin.internal)) 22 | (foreign_archives wasm) 23 | (c_library_flags 24 | (-lpthread -lc -lm))) 25 | 26 | (rule 27 | (targets libwasm.a dllwasm.so) 28 | (deps 29 | (glob_files *.rs)) 30 | (action 31 | (progn 32 | (run sh -c "cd %{project_root}/../.. && cargo build --release") 33 | (run 34 | sh 35 | -c 36 | "mv %{project_root}/../../target/release/libwasm.so ./dllwasm.so 2> /dev/null || mv %{project_root}/../../target/release/libwasm.dylib ./dllwasm.so") 37 | (run mv %{project_root}/../../target/release/libwasm.a libwasm.a)))) 38 | -------------------------------------------------------------------------------- /src/error.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | 3 | type t = [ `Msg of string | `Exception of exn ] 4 | type 'a res = ('a, t) result 5 | 6 | let to_string = function 7 | | `Msg s -> s 8 | | `Exception (Failure f) -> f 9 | | `Exception (Invalid_argument f) -> f 10 | | `Exception e -> Printexc.to_string e 11 | 12 | exception Wasmstore of t 13 | 14 | let () = 15 | Printexc.register_printer (function 16 | | Wasmstore error -> Some (to_string error) 17 | | _ -> None) 18 | 19 | let handle_error f = function 20 | | Wasmstore error -> f error 21 | | e -> f (`Exception e) 22 | 23 | let unwrap = function Ok x -> x | Error e -> raise (Wasmstore e) 24 | let wrap f = try Ok (f ()) with e -> handle_error Result.error e 25 | 26 | let unwrap_lwt x = 27 | let* x = x in 28 | Lwt.return (unwrap x) 29 | 30 | let wrap_lwt f = 31 | Lwt.catch 32 | (fun () -> 33 | let* x = f () in 34 | Lwt.return_ok x) 35 | (handle_error Lwt.return_error) 36 | 37 | let throw e = raise (Wasmstore e) 38 | let mk f = try f () with e -> handle_error throw e 39 | let mk_lwt f = Lwt.catch f (handle_error throw) 40 | let catch f g = try f () with e -> handle_error g e 41 | let catch_lwt f g = Lwt.catch f (handle_error g) 42 | -------------------------------------------------------------------------------- /src/gc.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Store 3 | module Hash_set = Set.Make (String) 4 | 5 | let rec get_first_parents repo commit = 6 | let parents = Store.Commit.parents commit in 7 | match parents with 8 | | [] -> Lwt.return [ `Commit (Store.Commit.key commit) ] 9 | | x -> 10 | let* x = 11 | Lwt_list.map_s 12 | (fun c -> 13 | let+ c = Store.Commit.of_key repo c in 14 | Option.get c) 15 | x 16 | in 17 | let+ p = Lwt_list.map_s (get_first_parents repo) x in 18 | List.flatten p 19 | 20 | let gc t = 21 | let repo = repo t in 22 | let* branches = Store.Branch.list repo in 23 | let info = info t "GC" () in 24 | let live = ref Hash_set.empty in 25 | let* max = 26 | Lwt_list.map_s 27 | (fun branch -> 28 | let* current = Store.Branch.get repo branch in 29 | match Store.Commit.parents current with 30 | | [] -> Lwt.return (`Commit (Store.Commit.key current)) 31 | | _ -> 32 | let+ commit = 33 | if String.equal branch t.branch then 34 | let* db = Store.of_branch repo branch in 35 | let* tree = Store.tree db in 36 | let* commit = Store.Commit.v repo ~info ~parents:[] tree in 37 | let+ () = Store.Branch.set repo branch commit in 38 | commit 39 | else Lwt.return current 40 | in 41 | `Commit (Store.Commit.key commit)) 42 | branches 43 | in 44 | let* min = 45 | Lwt_list.map_s 46 | (fun branch -> 47 | let* commit = Store.Branch.get repo branch in 48 | get_first_parents repo commit) 49 | branches 50 | in 51 | let min = List.flatten min in 52 | let node key = 53 | let+ tree = Store.Tree.of_key repo (`Node key) in 54 | let tree = Option.get tree in 55 | live := 56 | Hash_set.add 57 | (Irmin.Type.to_string Store.Hash.t @@ Store.Tree.hash tree) 58 | !live 59 | in 60 | let contents key = 61 | let+ c = Store.Contents.of_key repo key in 62 | let c = Option.get c in 63 | live := 64 | Hash_set.add 65 | (Irmin.Type.to_string Store.Hash.t @@ Store.Contents.hash c) 66 | !live 67 | in 68 | let commit key = 69 | let+ c = Store.Commit.of_hash repo key in 70 | let c = Option.get c in 71 | live := 72 | Hash_set.add 73 | (Irmin.Type.to_string Store.Hash.t @@ Store.Commit.hash c) 74 | !live 75 | in 76 | let* () = Store.Repo.iter ~min ~max ~node ~contents ~commit repo in 77 | let config = Store.Repo.config repo in 78 | let root = Irmin.Backend.Conf.get config Irmin_fs.Conf.Key.root in 79 | let objects = root // "objects" in 80 | let a = Lwt_unix.files_of_directory objects in 81 | let total = ref 0 in 82 | let* () = 83 | Lwt_stream.iter_p 84 | (fun path -> 85 | if path = "." || path = ".." then Lwt.return_unit 86 | else 87 | let b = Lwt_unix.files_of_directory (objects // path) in 88 | Lwt_stream.iter_s 89 | (fun f -> 90 | if f = "." || f = ".." then Lwt.return_unit 91 | else 92 | let hash = path ^ f in 93 | if not (Hash_set.mem hash !live) then 94 | let* () = Lwt_unix.unlink (objects // path // f) in 95 | let+ () = 96 | Lwt.catch 97 | (fun () -> Lwt_unix.rmdir (objects // path)) 98 | (fun _ -> Lwt.return_unit) 99 | in 100 | incr total 101 | else Lwt.return_unit) 102 | b) 103 | a 104 | in 105 | Lwt.return !total 106 | 107 | let gc t = Error.mk_lwt @@ fun () -> gc t 108 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use wasmparser::{Chunk, Parser, Payload::*, Validator, WasmFeatures}; 3 | 4 | fn err(x: T) -> String { 5 | x.to_string() 6 | } 7 | 8 | fn validate(mut reader: impl Read) -> Result<(), String> { 9 | let mut buf = Vec::new(); 10 | let mut parser = Parser::new(0); 11 | let mut eof = false; 12 | let mut stack = Vec::new(); 13 | let mut validator = Validator::new_with_features(WasmFeatures::all()); 14 | 15 | loop { 16 | let (payload, consumed) = match parser 17 | .parse(&buf, eof) 18 | .map_err(|x| x.message().to_string())? 19 | { 20 | Chunk::NeedMoreData(hint) => { 21 | if eof { 22 | return Err("unexpected end-of-file".to_string()); 23 | } 24 | 25 | // Use the hint to preallocate more space, then read 26 | // some more data into our buffer. 27 | // 28 | // Note that the buffer management here is not ideal, 29 | // but it's compact enough to fit in an example! 30 | let len = buf.len(); 31 | buf.extend((0..hint).map(|_| 0u8)); 32 | let n = reader.read(&mut buf[len..]).map_err(err)?; 33 | buf.truncate(len + n); 34 | eof = n == 0; 35 | continue; 36 | } 37 | 38 | Chunk::Parsed { consumed, payload } => (payload, consumed), 39 | }; 40 | 41 | match &payload { 42 | ModuleSection { parser: p, .. } | ComponentSection { parser: p, .. } => { 43 | stack.push(parser.clone()); 44 | parser = p.clone(); 45 | } 46 | _ => (), 47 | } 48 | 49 | match validator.payload(&payload).map_err(err)? { 50 | wasmparser::ValidPayload::End(_) => { 51 | if let Some(parent_parser) = stack.pop() { 52 | parser = parent_parser; 53 | } else { 54 | break; 55 | } 56 | } 57 | _ => (), 58 | } 59 | 60 | // once we're done processing the payload we can forget the 61 | // original. 62 | buf.drain(..consumed); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | fn return_string(mut s: String) -> *mut u8 { 69 | s.push('\0'); 70 | s.shrink_to_fit(); 71 | let ptr = s.as_ptr(); 72 | std::mem::forget(s); 73 | ptr as *mut _ 74 | } 75 | 76 | #[no_mangle] 77 | pub unsafe fn wasm_error_free(s: *mut u8) { 78 | let len = std::ffi::CStr::from_ptr(s as *const _).to_bytes().len() + 1; 79 | let s = String::from_raw_parts(s, len, len); 80 | drop(s) 81 | } 82 | 83 | #[no_mangle] 84 | pub unsafe fn wasm_verify_file(filename: *const u8, len: usize) -> *mut u8 { 85 | let slice = std::slice::from_raw_parts(filename, len); 86 | if let Ok(s) = std::str::from_utf8(slice) { 87 | let file = match std::fs::File::open(s) { 88 | Ok(f) => f, 89 | Err(_) => return return_string(format!("unable to open file {}", s)), 90 | }; 91 | 92 | match validate(file) { 93 | Ok(()) => std::ptr::null_mut(), 94 | Err(e) => return_string(e), 95 | } 96 | } else { 97 | std::ptr::null_mut() 98 | } 99 | } 100 | 101 | #[no_mangle] 102 | pub unsafe fn wasm_verify_string(filename: *const u8, len: usize) -> *mut u8 { 103 | let slice = std::slice::from_raw_parts(filename, len); 104 | match validate(slice) { 105 | Ok(()) => std::ptr::null_mut(), 106 | Err(s) => return_string(s), 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/rust.ml: -------------------------------------------------------------------------------- 1 | open Ctypes 2 | 3 | external _wasm_verify_file : unit -> unit = "wasm_verify_file" 4 | external _wasm_verify_string : unit -> unit = "wasm_verify_string" 5 | external _wasm_error_free : unit -> unit = "wasm_error_free" 6 | 7 | let fn = Foreign.foreign 8 | 9 | let wasm_verify_file = 10 | fn "wasm_verify_file" (ocaml_string @-> size_t @-> returning (ptr char)) 11 | 12 | let wasm_verify_string = 13 | fn "wasm_verify_string" (ocaml_string @-> size_t @-> returning (ptr char)) 14 | 15 | let wasm_error_free = fn "wasm_error_free" (ptr char @-> returning void) 16 | let clone_string s = Bytes.unsafe_of_string s |> Bytes.to_string 17 | 18 | let wrap res = 19 | if is_null res then Ok () 20 | else 21 | let s = coerce (ptr char) string res in 22 | let out = clone_string s in 23 | let () = wasm_error_free res in 24 | Error (`Msg out) 25 | 26 | let verify_file filename : (unit, [ `Msg of string ]) result = 27 | let len = Unsigned.Size_t.of_int (String.length filename) in 28 | wrap @@ wasm_verify_file (ocaml_string_start filename) len 29 | 30 | let verify_string str : (unit, [ `Msg of string ]) result = 31 | let len = Unsigned.Size_t.of_int (String.length str) in 32 | wrap @@ wasm_verify_string (ocaml_string_start str) len 33 | -------------------------------------------------------------------------------- /src/schema.ml: -------------------------------------------------------------------------------- 1 | include Irmin.Schema.KV (Irmin.Contents.String_v2) 2 | module Hash = Irmin.Hash.SHA256 3 | module Key = Irmin.Key.Of_hash (Hash) 4 | module Node = Irmin.Node.Make (Hash) (Path) (Metadata) 5 | module Commit = Irmin.Commit.Make (Hash) 6 | -------------------------------------------------------------------------------- /src/server.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | open Store 3 | open Gc 4 | open Diff 5 | open Cohttp 6 | open Cohttp_lwt 7 | open Cohttp_lwt_unix 8 | 9 | let () = Irmin.Backend.Watch.set_listen_dir_hook Irmin_watcher.hook 10 | 11 | let with_branch' t req = 12 | let h = Cohttp.Request.headers req in 13 | let branch = Cohttp.Header.get h "Wasmstore-Branch" in 14 | match branch with None -> t | Some branch -> with_branch t branch 15 | 16 | let response x = 17 | let+ x = x in 18 | `Response x 19 | 20 | let list_modules t ~headers path = 21 | let* modules = list t path in 22 | let modules = 23 | List.map 24 | (fun (k, v) -> 25 | ( Irmin.Type.to_string Store.Path.t k, 26 | `String (Irmin.Type.to_string Store.Hash.t v) )) 27 | modules 28 | in 29 | let body = Yojson.Safe.to_string (`Assoc modules) in 30 | response @@ Server.respond_string ~status:`OK ~headers ~body () 31 | 32 | let list_branches t ~headers = 33 | let* branches = Branch.list t in 34 | let body = 35 | Body.of_string (Irmin.Type.(to_json_string (list string)) branches) 36 | in 37 | response @@ Server.respond ~headers ~body ~status:`OK () 38 | 39 | let add_module t ~headers body path = 40 | let data = Body.to_stream body in 41 | Lwt.catch 42 | (fun () -> 43 | let* hash = import t path data in 44 | let body = Irmin.Type.to_string Store.Hash.t hash in 45 | response @@ Server.respond_string ~headers ~status:`OK ~body ()) 46 | (function 47 | | Validation_error msg -> 48 | response 49 | @@ Server.respond_string ~headers ~status:`Bad_request ~body:msg () 50 | | exn -> raise exn) 51 | 52 | let set_hash t ~headers hash path = 53 | let hash = Irmin.Type.of_string Store.Hash.t hash in 54 | match hash with 55 | | Ok hash -> 56 | Lwt.catch 57 | (fun () -> 58 | let* () = set t path hash in 59 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" ()) 60 | (function 61 | | Validation_error msg -> 62 | response 63 | @@ Server.respond_string ~headers ~status:`Bad_request ~body:msg 64 | () 65 | | exn -> raise exn) 66 | | Error _ -> 67 | response 68 | @@ Server.respond_string ~headers ~status:`Bad_request 69 | ~body:"invalid hash" () 70 | 71 | let find_module t ~headers path = 72 | let* filename = get_hash_and_filename t path in 73 | match filename with 74 | | Some (hash, filename) -> 75 | let headers = 76 | Header.add headers "Wasmstore-Hash" 77 | (Irmin.Type.to_string Store.Hash.t hash) 78 | in 79 | let headers = Header.add headers "Content-Type" "application/wasm" in 80 | response @@ Server.respond_file ~headers ~fname:(root t // filename) () 81 | | _ -> 82 | response @@ Server.respond_string ~headers ~status:`Not_found ~body:"" () 83 | 84 | let delete_module t ~headers path = 85 | let* () = remove t path in 86 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" () 87 | 88 | let find_hash t ~headers path = 89 | let* hash = hash t path in 90 | match hash with 91 | | Some hash -> 92 | let body = Body.of_string (Irmin.Type.to_string Store.Hash.t hash) in 93 | response @@ Server.respond ~headers ~status:`OK ~body () 94 | | None -> 95 | response @@ Server.respond_string ~headers ~status:`Not_found ~body:"" () 96 | 97 | let remove_prefix path = 98 | match path with "api" :: "v1" :: tl -> Some (`V1 tl) | _ -> None 99 | 100 | let require_auth t ~body ~auth ~headers req ~v1 = 101 | let uri = Request.uri req in 102 | let meth = Request.meth req in 103 | let path = Uri.path uri in 104 | let path' = 105 | String.split_on_char '/' path 106 | |> List.filter_map (function "" -> None | x -> Some x) 107 | in 108 | let path' = remove_prefix path' in 109 | Logs.info (fun l -> 110 | l "%s %s\n%s" 111 | (Code.string_of_method meth) 112 | path 113 | (Request.headers req |> Header.to_string |> String.trim)); 114 | let f t = 115 | match path' with 116 | | Some p -> v1 (with_branch' t req) (meth, p) 117 | | None -> 118 | let* () = Body.drain_body body in 119 | response 120 | @@ Server.respond_string ~headers ~status:`Not_found ~body:"" () 121 | in 122 | if Hashtbl.length auth = 0 then f t 123 | else 124 | let h = Request.headers req in 125 | let key = Header.get h "Wasmstore-Auth" |> Option.value ~default:"" in 126 | let perms = 127 | Hashtbl.find_opt auth key |> Option.map (String.split_on_char ',') 128 | in 129 | match perms with 130 | | Some [ "*" ] -> f t 131 | | Some x -> 132 | let exists = 133 | List.exists 134 | (fun m -> String.equal (Cohttp.Code.string_of_method meth) m) 135 | x 136 | in 137 | if exists then f t 138 | else 139 | response 140 | @@ Server.respond_string ~headers ~status:`Unauthorized ~body:"" () 141 | | None -> 142 | response 143 | @@ Server.respond_string ~headers ~status:`Unauthorized ~body:"" () 144 | 145 | (** [/api/v1] endpoints *) 146 | let v1 t ~headers ~body ~req = function 147 | | `GET, `V1 ("commit" :: [ hash ]) -> ( 148 | let hash' = Irmin.Type.of_string Hash.t hash in 149 | let fail body status = 150 | response @@ Server.respond_string ~headers ~status ~body () 151 | in 152 | match hash' with 153 | | Error _ -> fail "invalid hash" `Bad_request 154 | | Ok hash -> ( 155 | let* info = commit_info t hash in 156 | match info with 157 | | Some info -> 158 | let body = Irmin.Type.to_json_string Commit_info.t info in 159 | response @@ Server.respond_string ~headers ~status:`OK ~body () 160 | | None -> fail "invalid commit" `Not_found)) 161 | | `GET, `V1 ("modules" :: path) -> 162 | let* () = Body.drain_body body in 163 | list_modules t ~headers path 164 | | `GET, `V1 ("module" :: path) -> 165 | let* () = Body.drain_body body in 166 | find_module t ~headers path 167 | | `HEAD, `V1 ("module" :: path) -> 168 | let* () = Body.drain_body body in 169 | let* exists = contains t path in 170 | response 171 | @@ Server.respond_string ~headers 172 | ~status:(if exists then `OK else `Not_found) 173 | ~body:"" () 174 | | `GET, `V1 ("hash" :: path) -> 175 | let* () = Body.drain_body body in 176 | find_hash t ~headers path 177 | | `POST, `V1 ("hash" :: hash :: path) -> 178 | let* () = Body.drain_body body in 179 | set_hash t ~headers hash path 180 | | `POST, `V1 ("module" :: path) -> add_module t ~headers body path 181 | | `DELETE, `V1 ("module" :: path) -> 182 | let* () = Body.drain_body body in 183 | delete_module t ~headers path 184 | | `POST, `V1 [ "gc" ] -> 185 | let* () = Body.drain_body body in 186 | let* _ = gc t in 187 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" () 188 | | `POST, `V1 [ "merge"; from_branch ] -> ( 189 | let* res = merge t from_branch in 190 | match res with 191 | | Ok _ -> 192 | response @@ Server.respond_string ~status:`OK ~headers ~body:"" () 193 | | Error r -> 194 | response 195 | @@ Server.respond_string ~headers ~status:`Bad_request 196 | ~body:(Irmin.Type.to_string Irmin.Merge.conflict_t r) 197 | ()) 198 | | `POST, `V1 ("restore" :: hash :: path) -> ( 199 | let hash = Irmin.Type.of_string Store.Hash.t hash in 200 | match hash with 201 | | Error _ -> 202 | response 203 | @@ Server.respond_string ~headers ~status:`Bad_request 204 | ~body:"invalid hash in request" () 205 | | Ok hash -> ( 206 | let* commit = Store.Commit.of_hash (repo t) hash in 207 | match commit with 208 | | None -> 209 | response 210 | @@ Server.respond_string ~headers ~status:`Not_found 211 | ~body:"commit not found" () 212 | | Some commit -> 213 | let* () = restore ~path t commit in 214 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" () 215 | )) 216 | | `POST, `V1 ("rollback" :: path) -> 217 | let* () = rollback t ~path 1 in 218 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" () 219 | | `GET, `V1 [ "snapshot" ] -> 220 | let* commit = snapshot t in 221 | response 222 | @@ Server.respond_string ~headers ~status:`OK 223 | ~body:(Irmin.Type.to_string Store.Hash.t (Store.Commit.hash commit)) 224 | () 225 | | `GET, `V1 ("versions" :: path) -> 226 | let* versions = versions t path in 227 | let conv = Irmin.Type.to_string Hash.t in 228 | let versions = 229 | List.map 230 | (fun (k, `Commit v) -> `List [ `String (conv k); `String (conv v) ]) 231 | versions 232 | in 233 | let json = Yojson.Safe.to_string (`List versions) in 234 | let body = Body.of_string json in 235 | response @@ Server.respond ~headers ~body ~status:`OK () 236 | | `GET, `V1 ("version" :: v :: path) -> ( 237 | let* () = Body.drain_body body in 238 | let* version = version t path (int_of_string v) in 239 | match version with 240 | | None -> response @@ Server.respond_not_found () 241 | | Some (_, `Commit commit) -> 242 | let* commit = Store.Commit.of_hash (Store.repo t.db) commit in 243 | let* store = Store.of_commit (Option.get commit) in 244 | let t' = { t with db = store } in 245 | find_module t' ~headers path) 246 | | `GET, `V1 [ "branches" ] -> 247 | let* () = Body.drain_body body in 248 | list_branches t ~headers 249 | | `PUT, `V1 [ "branch"; branch ] -> 250 | let* () = Branch.switch t branch in 251 | response @@ Server.respond_string ~headers ~body:"" ~status:`OK () 252 | | `POST, `V1 [ "branch"; branch ] -> ( 253 | let* res = Branch.create t branch in 254 | match res with 255 | | Ok _ -> 256 | response @@ Server.respond_string ~headers ~status:`OK ~body:"" () 257 | | Error (`Msg s) -> 258 | response 259 | @@ Server.respond_string ~headers ~status:`Conflict ~body:s ()) 260 | | `DELETE, `V1 [ "branch"; branch ] -> 261 | let* () = Branch.delete t branch in 262 | response @@ Server.respond_string ~headers ~body:"" ~status:`OK () 263 | | `GET, `V1 [ "branch" ] -> 264 | response @@ Server.respond_string ~headers ~status:`OK ~body:t.branch () 265 | | `GET, `V1 [ "watch" ] -> 266 | let w = ref None in 267 | let* a, send = 268 | Server_websocket.upgrade_connection req (fun msg -> 269 | if msg.opcode = Websocket.Frame.Opcode.Close then 270 | match !w with 271 | | Some w -> Lwt.async (fun () -> Store.unwatch w) 272 | | None -> ()) 273 | in 274 | let+ watch = 275 | watch t (fun diff -> 276 | Lwt.catch 277 | (fun () -> 278 | let d = Yojson.Safe.to_string diff in 279 | Lwt.wrap (fun () -> 280 | send (Some (Websocket.Frame.create ~content:d ())))) 281 | (fun _ -> 282 | match !w with 283 | | Some w' -> 284 | let+ () = Store.unwatch w' in 285 | w := None 286 | | None -> Lwt.return_unit)) 287 | in 288 | w := Some watch; 289 | a 290 | | _, `V1 [ "auth" ] -> 291 | let* () = Body.drain_body body in 292 | response @@ Server.respond_string ~headers ~body:"" ~status:`OK () 293 | | _ -> 294 | let* () = Body.drain_body body in 295 | response @@ Server.respond_string ~headers ~body:"" ~status:`Not_found () 296 | 297 | let callback t ~headers ~auth _conn req body = 298 | require_auth t ~auth ~headers ~body req ~v1:(v1 ~headers ~body ~req) 299 | 300 | let run ?tls ?(cors = false) ?auth ?(host = "localhost") ?(port = 6384) t = 301 | let headers = 302 | if cors then Header.of_list [ ("Access-Control-Allow-Origin", "*") ] 303 | else Header.of_list [] 304 | in 305 | let auth : (string, string) Hashtbl.t = 306 | match auth with 307 | | Some x -> Hashtbl.of_seq (List.to_seq x) 308 | | None -> Hashtbl.create 0 309 | in 310 | let mode, tls_own_key = 311 | match tls with 312 | | Some (`Key_file kf, `Cert_file cf) -> 313 | let cert = `Crt_file_path cf in 314 | let key = `Key_file_path kf in 315 | ( `TLS (cert, key, `No_password, `Port port), 316 | Some (`TLS (cert, key, `No_password)) ) 317 | | None -> (`TCP (`Port port), None) 318 | in 319 | let* ctx = Conduit_lwt_unix.init ~src:host ?tls_own_key () in 320 | let ctx = Net.init ~ctx () in 321 | let callback = callback t ~headers ~auth in 322 | let server = Server.make_response_action ~callback () in 323 | Logs.app (fun l -> 324 | l "Starting server on %s:%d, cors=%b, tls=%b" host port cors 325 | (Option.is_some tls)); 326 | Server.create ~ctx ~on_exn:(fun _ -> ()) ~mode server 327 | -------------------------------------------------------------------------------- /src/server_websocket.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2016-2018 Maciej Wos 3 | * Copyright (c) 2012-2018 Vincent Bernardoff 4 | * 5 | * Permission to use, copy, modify, and distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | * 17 | *) 18 | 19 | open Lwt.Infix 20 | open Websocket 21 | module Lwt_IO = Websocket.Make (Cohttp_lwt_unix.Private.IO) 22 | 23 | let send_frames stream oc = 24 | let buf = Buffer.create 128 in 25 | let send_frame fr = 26 | Buffer.clear buf; 27 | Lwt_IO.write_frame_to_buf ~mode:Server buf fr; 28 | Lwt_io.write oc @@ Buffer.contents buf 29 | in 30 | Lwt_stream.iter_s send_frame stream 31 | 32 | let read_frames ic oc handler_fn = 33 | let read_frame = Lwt_IO.make_read_frame ~mode:Server ic oc in 34 | let rec inner () = read_frame () >>= Lwt.wrap1 handler_fn >>= inner in 35 | inner () 36 | 37 | let upgrade_connection request incoming_handler = 38 | let headers = Cohttp.Request.headers request in 39 | (match Cohttp.Header.get headers "sec-websocket-key" with 40 | | None -> 41 | Lwt.fail_invalid_arg 42 | "upgrade_connection: missing header `sec-websocket-key`" 43 | | Some key -> Lwt.return key) 44 | >>= fun key -> 45 | let hash = b64_encoded_sha1sum (key ^ websocket_uuid) in 46 | let response_headers = 47 | Cohttp.Header.of_list 48 | [ 49 | ("Upgrade", "websocket"); 50 | ("Connection", "Upgrade"); 51 | ("Sec-WebSocket-Accept", hash); 52 | ] 53 | in 54 | let resp = 55 | Cohttp.Response.make ~status:`Switching_protocols 56 | ~encoding:Cohttp.Transfer.Unknown ~headers:response_headers ~flush:true () 57 | in 58 | let frames_out_stream, frames_out_fn = Lwt_stream.create () in 59 | let f ic oc = 60 | Lwt.pick 61 | [ 62 | (* input: data from the client is read from the input channel 63 | * of the tcp connection; pass it to handler function *) 64 | read_frames ic oc incoming_handler; 65 | (* output: data for the client is written to the output 66 | * channel of the tcp connection *) 67 | send_frames frames_out_stream oc; 68 | ] 69 | in 70 | Lwt.return (`Expert (resp, f), frames_out_fn) 71 | -------------------------------------------------------------------------------- /src/store.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Syntax 2 | 3 | let ( // ) = Filename.concat 4 | 5 | module Store = Irmin_fs_unix.Make (Schema) 6 | module Info = Irmin_unix.Info (Store.Info) 7 | module Hash = Store.Hash 8 | 9 | type t = { 10 | mutable db : Store.t; 11 | env : Eio_unix.Stdenv.base; 12 | mutable branch : string; 13 | author : string; 14 | } 15 | 16 | type hash = Store.Hash.t 17 | 18 | let store { db; _ } = db 19 | let branch { branch; _ } = branch 20 | let repo { db; _ } = Store.repo db 21 | 22 | exception Validation_error of string 23 | 24 | let info t = Info.v ~author:t.author 25 | 26 | let hash_or_path ~hash ~path = function 27 | | [ hash_or_path ] -> ( 28 | match Irmin.Type.of_string Store.Hash.t hash_or_path with 29 | | Ok x -> Error.mk_lwt @@ fun () -> hash x 30 | | Error _ -> Error.mk_lwt @@ fun () -> path [ hash_or_path ]) 31 | | x -> Error.mk_lwt @@ fun () -> path x 32 | 33 | let root t = 34 | let conf = Store.Repo.config (repo t) in 35 | Irmin.Backend.Conf.get conf Irmin_fs.Conf.Key.root 36 | 37 | let try_mkdir ?(mode = 0o755) path = 38 | Lwt.catch (fun () -> Lwt_unix.mkdir path mode) (fun _ -> Lwt.return_unit) 39 | 40 | let v ?(author = "wasmstore") ?(branch = Store.Branch.main) root ~env = 41 | let* () = try_mkdir root in 42 | let config = Irmin_fs.config root in 43 | let* repo = Store.Repo.v config in 44 | let* db = Store.of_branch repo branch in 45 | let* () = try_mkdir (root // "tmp") in 46 | let* () = try_mkdir (root // "objects") in 47 | Lwt.return { db; branch; author; env } 48 | 49 | let verify_string wasm = 50 | match Rust.verify_string wasm with 51 | | Ok () -> () 52 | | Error (`Msg e) -> raise (Validation_error e) 53 | 54 | let verify_file filename = 55 | match Rust.verify_file filename with 56 | | Ok () -> () 57 | | Error (`Msg e) -> raise (Validation_error e) 58 | 59 | let snapshot { db; _ } = Store.Head.get db 60 | 61 | let restore t ?path commit = 62 | match path with 63 | | None | Some [] -> Error.mk_lwt @@ fun () -> Store.Head.set t.db commit 64 | | Some path -> 65 | let info = info t "Restore %a" (Irmin.Type.pp Store.Path.t) path in 66 | let parents = Store.Commit.parents commit in 67 | let* parents = 68 | Lwt_list.filter_map_s (Store.Commit.of_key (Store.repo t.db)) parents 69 | in 70 | let tree = Store.Commit.tree commit in 71 | Error.mk_lwt @@ fun () -> 72 | Store.with_tree_exn ~parents ~info t.db path (fun _ -> 73 | Store.Tree.find_tree tree path) 74 | 75 | let tree_opt_equal = Irmin.Type.(unstage (equal (option Store.Tree.t))) 76 | 77 | let rollback t ?(path = []) n : unit Lwt.t = 78 | let* lm = Store.last_modified ~n:(n + 1) t.db path in 79 | match List.rev lm with 80 | | commit :: _ :: _ -> restore t ~path commit 81 | | [ _ ] | [] -> 82 | let info = info t "Rollback %a" Irmin.Type.(pp Store.Path.t) path in 83 | Error.mk_lwt @@ fun () -> 84 | Store.with_tree_exn ~info t.db path (fun _ -> Lwt.return_none) 85 | 86 | let path_of_hash hash = 87 | let hash' = Irmin.Type.to_string Store.Hash.t hash in 88 | let a = String.sub hash' 0 2 in 89 | let b = String.sub hash' 2 (String.length hash' - 2) in 90 | "objects" // a // b 91 | 92 | let hash_eq = Irmin.Type.(unstage (equal Store.Hash.t)) 93 | 94 | let contains_hash t hash = 95 | let rec aux tree = 96 | match Store.Tree.destruct tree with 97 | | `Contents (c, _) -> 98 | let hash' = Store.Tree.Contents.hash c in 99 | Lwt.return @@ hash_eq hash hash' 100 | | `Node _ -> 101 | let* items = Store.Tree.list tree [] in 102 | Lwt_list.exists_p (fun (_, tree') -> aux tree') items 103 | in 104 | let* tree = Store.tree t.db in 105 | aux tree 106 | 107 | let get_hash_and_filename t path = 108 | let* hash = hash_or_path ~hash:Lwt.return_some ~path:(Store.hash t.db) path in 109 | match hash with 110 | | None -> Lwt.return_none 111 | | Some hash -> 112 | let+ exists = contains_hash t hash in 113 | if exists then 114 | let path = path_of_hash hash in 115 | Some (hash, path) 116 | else None 117 | 118 | let set_path t path hash = 119 | let* tree = Store.Tree.of_hash (repo t) (`Contents (hash, ())) in 120 | match tree with 121 | | None -> Error.throw (`Msg "hash mismatch") 122 | | Some tree -> 123 | let info = info t "Import %a" (Irmin.Type.pp Store.Path.t) path in 124 | Store.set_tree_exn t.db path tree ~info 125 | 126 | let import t path stream = 127 | let hash = ref (Digestif.SHA256.init ()) in 128 | let tmp = 129 | Filename.temp_file ~temp_dir:(root t // "tmp") "wasmstore" "import" 130 | in 131 | let* () = 132 | Lwt_io.with_file 133 | ~flags:Unix.[ O_CREAT; O_WRONLY ] 134 | ~mode:Output tmp 135 | (fun oc -> 136 | Lwt_stream.iter_s 137 | (fun s -> 138 | hash := Digestif.SHA256.feed_string !hash s; 139 | Lwt_io.write oc s) 140 | stream) 141 | in 142 | let hash = Digestif.SHA256.get !hash in 143 | let hash = 144 | Irmin.Hash.SHA256.unsafe_of_raw_string (Digestif.SHA256.to_raw_string hash) 145 | in 146 | let dest = root t // path_of_hash hash in 147 | let* exists = Lwt_unix.file_exists dest in 148 | let* () = 149 | if not exists then 150 | let () = 151 | try verify_file tmp 152 | with e -> 153 | Unix.unlink tmp; 154 | raise e 155 | in 156 | let* () = try_mkdir (Filename.dirname dest) in 157 | Lwt_unix.rename tmp dest 158 | else Lwt.return_unit 159 | in 160 | let* () = set_path t path hash in 161 | Lwt.return hash 162 | 163 | let add t path wasm = 164 | let () = verify_string wasm in 165 | let info = info t "Add %a" (Irmin.Type.pp Store.Path.t) path in 166 | let f hash = 167 | Error.mk_lwt @@ fun () -> 168 | Store.set_exn t.db [ Irmin.Type.to_string Store.Hash.t hash ] wasm ~info 169 | in 170 | let+ () = 171 | hash_or_path ~hash:f 172 | ~path:(fun path -> 173 | (* If the path is empty then just add the contents to the store without 174 | associating it with a path *) 175 | match path with 176 | | [] -> 177 | Store.Backend.Repo.batch (repo t) (fun contents _ _ -> 178 | let+ _ = Store.save_contents contents wasm in 179 | ()) 180 | | _ -> Error.mk_lwt @@ fun () -> Store.set_exn t.db path wasm ~info) 181 | path 182 | in 183 | Store.Contents.hash wasm 184 | 185 | let set t path hash = 186 | let* tree = Store.Tree.of_hash (repo t) (`Contents (hash, ())) in 187 | let f path = 188 | match tree with 189 | | None -> Error.throw (`Msg "hash mismatch") 190 | | Some tree -> 191 | let info = 192 | info t "Set %a %a" 193 | (Irmin.Type.pp Store.Path.t) 194 | path 195 | (Irmin.Type.pp Store.Hash.t) 196 | hash 197 | in 198 | Store.set_tree_exn t.db path tree ~info 199 | in 200 | hash_or_path 201 | ~hash:(fun _ -> 202 | Error.throw (`Msg "A hash path should not be used with `set` command")) 203 | ~path:f path 204 | 205 | let find_hash t hash = 206 | let* contains = contains_hash t hash in 207 | if contains then Store.Contents.of_hash (Store.repo t.db) hash 208 | else Lwt.return_none 209 | 210 | let find t path = hash_or_path ~hash:(find_hash t) ~path:(Store.find t.db) path 211 | 212 | let hash t path = 213 | hash_or_path ~hash:(fun x -> Lwt.return_some x) ~path:(Store.hash t.db) path 214 | 215 | let remove t path = 216 | let info = info t "Remove %a" (Irmin.Type.pp Store.Path.t) path in 217 | let hash h = 218 | (* Search through the current tree for any contents that match [h] *) 219 | let rec aux tree = 220 | match Store.Tree.destruct tree with 221 | | `Contents (c, _) -> 222 | let hash = Store.Tree.Contents.hash c in 223 | if hash_eq hash h then Store.Tree.remove tree [] else Lwt.return tree 224 | | `Node _ -> 225 | let* items = Store.Tree.list tree [] in 226 | Lwt_list.fold_left_s 227 | (fun tree -> function 228 | | p, tree' -> 229 | let* x = aux tree' in 230 | Store.Tree.add_tree tree [ p ] x) 231 | tree items 232 | in 233 | let* tree = Store.tree t.db in 234 | let is_empty = Store.Tree.is_empty tree in 235 | let* tree' = aux tree in 236 | Error.mk_lwt @@ fun () -> 237 | Store.test_and_set_tree_exn t.db [] 238 | ~test:(if is_empty then None else Some tree) 239 | ~set:(Some tree') ~info 240 | in 241 | hash_or_path ~path:(Store.remove_exn t.db ~info) ~hash path 242 | 243 | let list { db; _ } path = 244 | let rec aux path = 245 | let* items = Store.list db path in 246 | let+ items = 247 | Lwt_list.map_s 248 | (fun (k, v) -> 249 | let full = Store.Path.rcons path k in 250 | let* kind = Store.Tree.kind v [] in 251 | match kind with 252 | | None -> Lwt.return [] 253 | | Some `Contents -> Lwt.return [ (full, Store.Tree.hash v) ] 254 | | Some `Node -> aux full) 255 | items 256 | in 257 | List.flatten items 258 | in 259 | aux path 260 | 261 | let contains t path = 262 | hash_or_path 263 | ~hash:(fun h -> contains_hash t h) 264 | ~path:(fun path -> Store.mem t.db path) 265 | path 266 | 267 | let merge t branch = 268 | let info = info t "Merge %s" branch in 269 | Store.merge_with_branch t.db ~info branch 270 | 271 | let with_branch t branch = { t with branch } 272 | let with_author t author = { t with author } 273 | 274 | module Hash_set = Set.Make (struct 275 | type t = Hash.t 276 | 277 | let compare = Irmin.Type.(unstage @@ compare Hash.t) 278 | end) 279 | 280 | let versions t path = 281 | let* lm = Store.last_modified t.db ~n:max_int path in 282 | let hashes = ref Hash_set.empty in 283 | Lwt_list.filter_map_s 284 | (fun commit -> 285 | let* store = Store.of_commit commit in 286 | let* hash = Store.hash store path in 287 | match hash with 288 | | None -> Lwt.return_none 289 | | Some h -> 290 | if Hash_set.mem h !hashes then Lwt.return_none 291 | else 292 | let () = hashes := Hash_set.add h !hashes in 293 | Lwt.return_some (h, `Commit (Store.Commit.hash commit))) 294 | lm 295 | 296 | let version t path index = 297 | let+ versions = versions t path in 298 | List.nth_opt versions index 299 | 300 | module Commit_info = struct 301 | type t = { 302 | hash : Hash.t; 303 | parents : Hash.t list; 304 | author : string; 305 | date : int64; 306 | message : string; 307 | } 308 | [@@deriving irmin] 309 | end 310 | 311 | let commit_info t hash = 312 | let* commit = 313 | Lwt.catch 314 | (fun () -> Store.Commit.of_hash (repo t) hash) 315 | (function Assert_failure _ -> Lwt.return_none | exn -> raise exn) 316 | in 317 | match commit with 318 | | Some commit -> 319 | let parents = Store.Commit.parents commit in 320 | let info = Store.Commit.info commit in 321 | Lwt.return_some 322 | Commit_info. 323 | { 324 | hash; 325 | parents; 326 | author = Store.Info.author info; 327 | date = Store.Info.date info; 328 | message = Store.Info.message info; 329 | } 330 | | None -> Lwt.return_none 331 | -------------------------------------------------------------------------------- /src/wasmstore.ml: -------------------------------------------------------------------------------- 1 | include Store 2 | include Gc 3 | module Branch = Branch 4 | module Server = Server 5 | module Error = Error 6 | 7 | let watch = Diff.watch 8 | let unwatch = Store.unwatch 9 | -------------------------------------------------------------------------------- /src/wasmstore.mli: -------------------------------------------------------------------------------- 1 | (** [Wasmstore] is a database used to securely store WebAssembly modules *) 2 | 3 | (** The underlying irmin store *) 4 | module Store : 5 | Irmin.S 6 | with type Schema.Contents.t = string 7 | and type Schema.Path.t = string list 8 | and type Schema.Branch.t = string 9 | and type hash = Irmin.Hash.SHA256.t 10 | and module Schema.Info = Irmin.Info.Default 11 | 12 | exception Validation_error of string 13 | 14 | (** Error type and convenience functions *) 15 | 16 | module Error : sig 17 | type t = [ `Msg of string | `Exception of exn ] 18 | type 'a res = ('a, t) result 19 | 20 | exception Wasmstore of t 21 | 22 | val to_string : t -> string 23 | val unwrap : ('a, t) result -> 'a 24 | val unwrap_lwt : ('a, t) result Lwt.t -> 'a Lwt.t 25 | val wrap : (unit -> 'a) -> ('a, t) result 26 | val wrap_lwt : (unit -> 'a Lwt.t) -> ('a, t) result Lwt.t 27 | val throw : t -> 'a 28 | val catch_lwt : (unit -> 'a Lwt.t) -> (t -> 'a Lwt.t) -> 'a Lwt.t 29 | val catch : (unit -> 'a) -> (t -> 'a) -> 'a 30 | end 31 | 32 | type t 33 | (** The main [Wasmstore] type *) 34 | 35 | type hash = Irmin.Hash.SHA256.t 36 | (** Hash type, SHA256 *) 37 | 38 | module Hash : Irmin.Hash.S with type t = hash 39 | (** Re-export of [Store.Hash] *) 40 | 41 | val branch : t -> string 42 | (** [branch t] returns the current branch *) 43 | 44 | val store : t -> Store.t 45 | (** [store t] returns the underlying irmin store *) 46 | 47 | val repo : t -> Store.repo 48 | (** [repo t] returns the underlying irmin repo *) 49 | 50 | val v : 51 | ?author:string -> 52 | ?branch:string -> 53 | string -> 54 | env:Eio_unix.Stdenv.base -> 55 | t Lwt.t 56 | (** [v ~branch root] opens a store open to [branch] on disk at [root] *) 57 | 58 | val snapshot : t -> Store.commit Lwt.t 59 | (** [snapshot t] gets the current head commit *) 60 | 61 | val restore : t -> ?path:string list -> Store.commit -> unit Lwt.t 62 | (** [restore t commit] sets the head commit, if [path] is provided then only the 63 | specfied path will be reverted *) 64 | 65 | val rollback : t -> ?path:string list -> int -> unit Lwt.t 66 | (** [rollback t n] sets the head commit to [n] commits in the past, if [path] is 67 | provided then only the specfied path will be reverted *) 68 | 69 | val find : t -> string list -> string option Lwt.t 70 | (** [find t path] returns the module associated with [path], if path is a 71 | single-item list containing the string representation of the hash then the 72 | module will be located using the hash instead. This goes for all functions 73 | that accept [path] arguments unless otherwise noted. *) 74 | 75 | val add : t -> string list -> string -> hash Lwt.t 76 | (** [add t path wasm_module] sets [path] to [wasm_module] after verifying the 77 | module. If [path] is a hash then it will be converted to "[$HASH].wasm". *) 78 | 79 | val set : t -> string list -> hash -> unit Lwt.t 80 | (** [set t path hash] sets [path] to an existing [hash] *) 81 | 82 | val import : t -> string list -> string Lwt_stream.t -> hash Lwt.t 83 | (** [import t path stream] adds a WebAssembly module from the given stream *) 84 | 85 | val hash : t -> string list -> hash option Lwt.t 86 | (** [hash t path] returns the hash associated the the value stored at [path], if 87 | it exists *) 88 | 89 | val remove : t -> string list -> unit Lwt.t 90 | (** [remove t path] deletes [path] *) 91 | 92 | val list : t -> string list -> (string list * hash) list Lwt.t 93 | (** [list t path] returns a list of modules stored under [path]. This function 94 | does not accept a hash parameter in place of [path] *) 95 | 96 | val contains : t -> string list -> bool Lwt.t 97 | (** [contains t path] returns true if [path] exists *) 98 | 99 | val gc : t -> int Lwt.t 100 | (** [gc t] runs the GC and returns the number of objects deleted. 101 | 102 | When the gc is executed for a branch all prior commits are squashed into one 103 | and all non-reachable objects are removed. For example, if an object is 104 | still reachable from another branch it will not be deleted. Because of this, 105 | running the garbage collector may purge prior commits, potentially causing 106 | `restore` to fail. *) 107 | 108 | val get_hash_and_filename : t -> string list -> (hash * string) option Lwt.t 109 | (** [get_hash_and_filename t path] returns a tuple containing the hash and the 110 | filename of the object disk relative to the root path *) 111 | 112 | val merge : t -> string -> (unit, Irmin.Merge.conflict) result Lwt.t 113 | (** [merge t branch] merges [branch] into [t] *) 114 | 115 | val with_branch : t -> string -> t 116 | (** [with_branch t branch] returns a copy of [t] with [branch] selected *) 117 | 118 | val with_author : t -> string -> t 119 | (** [with_author t name] returns a copy of [t] with [name] as the current author *) 120 | 121 | val watch : t -> (Yojson.Safe.t -> unit Lwt.t) -> Store.watch Lwt.t 122 | (** [watch t f] creates a new watch that calls [f] for each new commit *) 123 | 124 | val unwatch : Store.watch -> unit Lwt.t 125 | (** [unwatch w] unregisters and disables the watch [w] *) 126 | 127 | val versions : t -> string list -> (hash * [ `Commit of hash ]) list Lwt.t 128 | 129 | val version : 130 | t -> string list -> int -> (hash * [ `Commit of hash ]) option Lwt.t 131 | 132 | module Commit_info : sig 133 | type t = { 134 | hash : Hash.t; 135 | parents : Hash.t list; 136 | author : string; 137 | date : int64; 138 | message : string; 139 | } 140 | [@@deriving irmin] 141 | end 142 | 143 | val commit_info : t -> hash -> Commit_info.t option Lwt.t 144 | 145 | module Branch : sig 146 | val switch : t -> string -> unit Lwt.t 147 | (** [switch t branch] sets [t]'s branch to [branch] *) 148 | 149 | val create : t -> string -> t Error.res Lwt.t 150 | (** [create t branch] creates a new branch, returning an error result if the 151 | branch already exists *) 152 | 153 | val delete : t -> string -> unit Lwt.t 154 | (** [delete t branch] destroys [branch] *) 155 | 156 | val list : t -> string list Lwt.t 157 | (** [list t] returns a list of all branches *) 158 | end 159 | 160 | module Server : sig 161 | val run : 162 | ?tls:[ `Key_file of string ] * [ `Cert_file of string ] -> 163 | ?cors:bool -> 164 | ?auth:(string * string) list -> 165 | ?host:string -> 166 | ?port:int -> 167 | t -> 168 | unit Lwt.t 169 | (** [run ~cors ~auth ~host ~port t] starts the server on [host:port] If [auth] 170 | is empty then no authentication is required, otherwise the client should 171 | provide a key using the [Wasmstore-Auth] header. [auth] is a mapping from 172 | authentication keys to allowed request methods (or [*] as a shortcut for 173 | any method). The `cors` parameters will enable CORS when set to true, 174 | allowing for browser-based Javascript clients to make requests agains the 175 | database. 176 | 177 | Additionally, the [Wasmstore-Branch] header can used to determine which 178 | branch to access on any non-[/branch] endpoints *) 179 | end 180 | -------------------------------------------------------------------------------- /test/a.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylibso/wasmstore/50b3050786ef5e4ab3925f7874d6cedb615e3476/test/a.wasm -------------------------------------------------------------------------------- /test/b.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylibso/wasmstore/50b3050786ef5e4ab3925f7874d6cedb615e3476/test/b.wasm -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (deps %{bin:wasmstore} a.wasm b.wasm)) 3 | -------------------------------------------------------------------------------- /test/wasmstore.ml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylibso/wasmstore/50b3050786ef5e4ab3925f7874d6cedb615e3476/test/wasmstore.ml -------------------------------------------------------------------------------- /test/wasmstore.t: -------------------------------------------------------------------------------- 1 | $ export WASMSTORE_ROOT=./test/tmp 2 | 3 | Parsing fail 4 | $ head -c 12 a.wasm | wasmstore add - a.wasm 5 | ERROR invalid module: unexpected end-of-file 6 | 7 | Add wasm module `a` 8 | $ cat a.wasm | wasmstore add - a.wasm 9 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 10 | 11 | Rollback `a` 12 | $ wasmstore rollback a.wasm 13 | $ wasmstore contains a.wasm 14 | false 15 | $ wasmstore add a.wasm 16 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 17 | 18 | Add wasm module `b` 19 | $ wasmstore add b.wasm 20 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 21 | 22 | Store contains `b` 23 | $ wasmstore contains b.wasm 24 | true 25 | 26 | Make sure the store contains the hash and path 27 | $ wasmstore contains b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 28 | true 29 | $ wasmstore contains a.wasm 30 | true 31 | 32 | Snapshot 1 33 | $ export SNAPSHOT1=`wasmstore snapshot` 34 | 35 | Make a new branch 36 | $ wasmstore branch test 37 | 38 | Remove `a` 39 | $ wasmstore remove a.wasm 40 | 41 | Snapshot 2 42 | $ export SNAPSHOT2=`wasmstore snapshot` 43 | 44 | Restore 1 45 | $ wasmstore restore $SNAPSHOT1 46 | $ wasmstore contains a.wasm 47 | true 48 | 49 | Versions 50 | $ wasmstore versions b.wasm | awk '{ print $1 }' 51 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 52 | 53 | Restore 2 54 | $ wasmstore restore $SNAPSHOT2 55 | 56 | Versions 57 | $ wasmstore add a.wasm b.wasm 58 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 59 | $ wasmstore versions b.wasm | awk '{ print $1 }' 60 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 61 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 62 | 63 | Restore 2 64 | $ wasmstore restore $SNAPSHOT2 65 | 66 | Store should no longer contain `a.wasm` 67 | $ wasmstore contains a.wasm 68 | false 69 | $ wasmstore list 70 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 /b.wasm 71 | 72 | Run garbage collector 73 | $ wasmstore gc 74 | 3 75 | 76 | Remove branch 77 | $ wasmstore branch test --delete 78 | 79 | Run garbage collector 80 | $ wasmstore gc 81 | 8 82 | 83 | Invalid WASM module 84 | $ head -c 5 a.wasm | wasmstore add - invalid.wasm 85 | ERROR invalid module: unexpected end-of-file 86 | 87 | Versions 88 | $ wasmstore add a.wasm b.wasm 89 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 90 | $ wasmstore versions b.wasm | awk '{ print $1 }' 91 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 92 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 93 | 94 | Set 95 | $ wasmstore set d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 c.wasm 96 | $ wasmstore hash c.wasm 97 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 98 | 99 | $ wasmstore set d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 b.wasm 100 | $ wasmstore versions b.wasm | awk '{ print $1 }' 101 | d926c50304238d423d63f52f5f460b1a7170fe870e10f031b9cbd74b29bc06e5 102 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 103 | 104 | Export 105 | $ wasmstore export -o ./exported 106 | $ ls ./exported 107 | b.wasm 108 | c.wasm 109 | 110 | Backup 111 | $ wasmstore backup backup.tar.gz 112 | $ tar tzf ./backup.tar.gz | grep 'objects/65/8830c0dfcc89d80c695357f0774eb20ca47adb4286eedd52eb527f9cf03fd5' 113 | ./objects/65/8830c0dfcc89d80c695357f0774eb20ca47adb4286eedd52eb527f9cf03fd5 114 | 115 | Add `a` again 116 | $ wasmstore add a.wasm testing/123 117 | b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 118 | 119 | Contains `a` 120 | $ wasmstore contains testing/123 121 | true 122 | 123 | Remove `a` by hash 124 | $ wasmstore remove b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 125 | 126 | No longer contains `a` 127 | $ wasmstore contains b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 128 | false 129 | 130 | No longer contains `a` 131 | $ wasmstore contains testing/123 132 | false 133 | 134 | No longer contains `a` 135 | $ wasmstore find b6b033aa8c568449d19e0d440cd31f8fcebaebc9c28070e09073275d8062be31 > /dev/null 136 | [1] 137 | -------------------------------------------------------------------------------- /wasmstore.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A WASM datastore" 4 | description: 5 | "An OCaml library and command line program used to store WebAssembly modules" 6 | maintainer: ["Zach Shipko "] 7 | authors: ["Dylibso Inc."] 8 | license: "BSD-3-clause" 9 | tags: ["topics" "wasm" "database" "irmin"] 10 | homepage: "https://github.com/dylibso/wasmstore" 11 | doc: "https://github.com/dylibso/wasmstore" 12 | bug-reports: "https://github.com/dylibso/wasmstore/issues" 13 | depends: [ 14 | "ocaml" 15 | "dune" {>= "3.2"} 16 | "irmin" 17 | "irmin-fs" 18 | "irmin-watcher" 19 | "lwt" 20 | "lwt_eio" 21 | "eio_main" 22 | "cohttp-lwt-unix" {>= "6.0.0~alpha2"} 23 | "fmt" 24 | "logs" 25 | "websocket" 26 | "cmdliner" {>= "1.0.0"} 27 | "yojson" 28 | "ctypes" 29 | "ctypes-foreign" 30 | "conf-rust-2021" 31 | "odoc" {with-doc} 32 | ] 33 | build: [ 34 | ["dune" "subst"] {dev} 35 | [ 36 | "dune" 37 | "build" 38 | "-p" 39 | name 40 | "-j" 41 | jobs 42 | "@install" 43 | "@runtest" {with-test} 44 | "@doc" {with-doc} 45 | ] 46 | ] 47 | dev-repo: "git+https://github.com/dylibso/wasmstore.git" 48 | --------------------------------------------------------------------------------