├── .cargo └── config.toml ├── .gitattributes ├── .github └── workflows │ ├── build-docker.yml │ └── deploy.yml ├── .gitignore ├── .vscode └── settings.json ├── COMMAND_PLAN.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docker ├── Dockerfile └── README.md ├── docs ├── .editorconfig ├── .gitignore ├── .markdownlint.json ├── .markdownlintignore ├── .npmrc ├── .nvmrc ├── .stylelintrc.cjs ├── .stylintrc ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── src │ ├── .vitepress │ │ ├── cache │ │ │ └── deps │ │ │ │ ├── @iconify-prerendered_vue-mdi.js │ │ │ │ ├── @iconify-prerendered_vue-mdi.js.map │ │ │ │ ├── @tanstack_vue-query.js │ │ │ │ ├── @tanstack_vue-query.js.map │ │ │ │ ├── _metadata.json │ │ │ │ ├── axios.js │ │ │ │ ├── axios.js.map │ │ │ │ ├── chunk-HL2QZUHZ.js │ │ │ │ ├── chunk-HL2QZUHZ.js.map │ │ │ │ ├── chunk-I3RIQLCS.js │ │ │ │ ├── chunk-I3RIQLCS.js.map │ │ │ │ ├── chunk-T3FA6UVC.js │ │ │ │ ├── chunk-T3FA6UVC.js.map │ │ │ │ ├── chunk-X4ECCFDQ.js │ │ │ │ ├── chunk-X4ECCFDQ.js.map │ │ │ │ ├── chunk-Y2XQL4KM.js │ │ │ │ ├── chunk-Y2XQL4KM.js.map │ │ │ │ ├── element-plus.js │ │ │ │ ├── element-plus.js.map │ │ │ │ ├── element-plus_es_components_loading_style_css.js │ │ │ │ ├── element-plus_es_components_loading_style_css.js.map │ │ │ │ ├── lodash__groupby.js │ │ │ │ ├── lodash__groupby.js.map │ │ │ │ ├── markdown-it.js │ │ │ │ ├── markdown-it.js.map │ │ │ │ ├── naive-ui.js │ │ │ │ ├── naive-ui.js.map │ │ │ │ ├── package.json │ │ │ │ ├── vitepress-plugin-tabs_client.js │ │ │ │ ├── vitepress-plugin-tabs_client.js.map │ │ │ │ ├── vitepress___@vue_devtools-api.js │ │ │ │ ├── vitepress___@vue_devtools-api.js.map │ │ │ │ ├── vitepress___@vueuse_core.js │ │ │ │ ├── vitepress___@vueuse_core.js.map │ │ │ │ ├── vue.js │ │ │ │ └── vue.js.map │ │ ├── config.ts │ │ ├── config │ │ │ ├── constants.ts │ │ │ ├── headConfig.ts │ │ │ ├── markdownConfig.ts │ │ │ ├── navigation │ │ │ │ ├── navbar.ts │ │ │ │ └── sidebar.ts │ │ │ ├── scripts │ │ │ │ └── languages.ts │ │ │ ├── shortcodes.ts │ │ │ └── themeConfig.ts │ │ ├── fonts │ │ │ ├── Inter-Bold.otf │ │ │ ├── Inter-Medium.otf │ │ │ ├── Inter-Regular.otf │ │ │ └── Inter-SemiBold.otf │ │ ├── theme │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ │ ├── AddRepoButton.vue │ │ │ │ ├── ChangelogsList.vue │ │ │ │ ├── CommandsCard.vue │ │ │ │ ├── Contributors.vue │ │ │ │ ├── CustomSwitchAppearance.vue │ │ │ │ ├── DownloadButtons.vue │ │ │ │ ├── Extensions │ │ │ │ │ ├── ExtensionFilters.vue │ │ │ │ │ ├── ExtensionGroup.vue │ │ │ │ │ ├── ExtensionItem.vue │ │ │ │ │ ├── ExtensionList.vue │ │ │ │ │ └── ExtensionsWrapper.vue │ │ │ │ ├── News.vue │ │ │ │ ├── OgImageTemplate.vue │ │ │ │ ├── ReleaseDate.vue │ │ │ │ └── RssLink.vue │ │ │ ├── data │ │ │ │ ├── changelogs.data.ts │ │ │ │ ├── news.data.ts │ │ │ │ └── release.data.ts │ │ │ ├── index.ts │ │ │ ├── plugin │ │ │ │ └── analytics.ts │ │ │ ├── queries │ │ │ │ └── useExtensionsRepositoryQuery.ts │ │ │ └── styles │ │ │ │ ├── base.styl │ │ │ │ ├── forks │ │ │ │ └── lint.styl │ │ │ │ └── tree.styl │ │ └── vue-shim.d.ts │ ├── docs │ │ ├── advance │ │ │ ├── persistence.md │ │ │ └── security.md │ │ ├── commands │ │ │ ├── generic.md │ │ │ ├── generic │ │ │ │ └── auth.md │ │ │ ├── hash.md │ │ │ ├── hash │ │ │ │ ├── hdel.md │ │ │ │ └── hset.md │ │ │ ├── key.md │ │ │ ├── key │ │ │ │ ├── del.md │ │ │ │ ├── rename.md │ │ │ │ ├── ttl.md │ │ │ │ └── type.md │ │ │ ├── list.md │ │ │ ├── list │ │ │ │ ├── llen.md │ │ │ │ └── lpop.md │ │ │ ├── set.md │ │ │ ├── set │ │ │ │ ├── sadd.md │ │ │ │ └── spop.md │ │ │ ├── sortedSet.md │ │ │ ├── sortedSet │ │ │ │ └── zadd.md │ │ │ ├── string.md │ │ │ └── string │ │ │ │ ├── get.md │ │ │ │ └── set.md │ │ └── guides │ │ │ ├── changelog.md │ │ │ ├── configuration.md │ │ │ ├── install.md │ │ │ ├── introduce.md │ │ │ └── protocolSpec.md │ ├── index.md │ ├── public │ │ ├── browserconfig.xml │ │ └── imgs │ │ │ ├── favicon.ico │ │ │ └── logo.png │ └── zh │ │ ├── docs │ │ ├── advance │ │ │ ├── persistence.md │ │ │ └── security.md │ │ ├── commands │ │ │ ├── generic.md │ │ │ ├── generic │ │ │ │ └── auth.md │ │ │ ├── hash.md │ │ │ ├── hash │ │ │ │ ├── hdel.md │ │ │ │ └── hset.md │ │ │ ├── key.md │ │ │ ├── key │ │ │ │ ├── del.md │ │ │ │ ├── rename.md │ │ │ │ ├── ttl.md │ │ │ │ └── type.md │ │ │ ├── list.md │ │ │ ├── list │ │ │ │ ├── llen.md │ │ │ │ └── lpop.md │ │ │ ├── set.md │ │ │ ├── set │ │ │ │ ├── sadd.md │ │ │ │ └── spop.md │ │ │ ├── sortedSet.md │ │ │ ├── sortedSet │ │ │ │ └── zadd.md │ │ │ ├── string.md │ │ │ └── string │ │ │ │ ├── get.md │ │ │ │ └── set.md │ │ └── guides │ │ │ ├── changelog.md │ │ │ ├── configuration.md │ │ │ ├── install.md │ │ │ ├── introduce.md │ │ │ └── protocolSpec.md │ │ └── index.md └── tsconfig.json ├── examples ├── demo.py ├── flagged │ └── log.csv ├── performance_comparison.png ├── rudis_visualizer.png └── rudis_visualizer.py ├── logo ├── logo-padding.png ├── logo-with-title.png └── logo.png ├── release ├── linux │ ├── rudis-server │ └── rudis-server.properties ├── macOS │ ├── rudis-server │ └── rudis-server.properties └── windows │ ├── rudis-server.exe │ └── rudis-server.properties ├── src ├── args.rs ├── cmd │ ├── auth.rs │ ├── dbsize.rs │ ├── echo.rs │ ├── flushall.rs │ ├── flushdb.rs │ ├── hash │ │ ├── hdel.rs │ │ ├── hexists.rs │ │ ├── hget.rs │ │ ├── hgetall.rs │ │ ├── hkeys.rs │ │ ├── hlen.rs │ │ ├── hmget.rs │ │ ├── hmset.rs │ │ ├── hset.rs │ │ ├── hsetnx.rs │ │ ├── hstrlen.rs │ │ ├── hvals.rs │ │ └── mod.rs │ ├── key │ │ ├── del.rs │ │ ├── exists.rs │ │ ├── expire.rs │ │ ├── expireat.rs │ │ ├── keys.rs │ │ ├── mod.rs │ │ ├── persist.rs │ │ ├── pexpire.rs │ │ ├── pexpireat.rs │ │ ├── pttl.rs │ │ ├── randomkey.rs │ │ ├── rename.rs │ │ ├── renamenx.rs │ │ ├── ttl.rs │ │ └── type.rs │ ├── list │ │ ├── lindex.rs │ │ ├── llen.rs │ │ ├── lpop.rs │ │ ├── lpush.rs │ │ ├── lpushx.rs │ │ ├── lrange.rs │ │ ├── lset.rs │ │ ├── mod.rs │ │ ├── rpop.rs │ │ ├── rpush.rs │ │ └── rpushx.rs │ ├── mod.rs │ ├── ping.rs │ ├── select.rs │ ├── set │ │ ├── mod.rs │ │ ├── sadd.rs │ │ ├── scard.rs │ │ ├── sinter.rs │ │ ├── sismember.rs │ │ ├── smembers.rs │ │ ├── spop.rs │ │ ├── srem.rs │ │ ├── sunion.rs │ │ └── sunionstore.rs │ ├── sorted_set │ │ ├── mod.rs │ │ ├── zadd.rs │ │ ├── zcard.rs │ │ ├── zcount.rs │ │ ├── zrank.rs │ │ ├── zrem.rs │ │ └── zscore.rs │ ├── string │ │ ├── append.rs │ │ ├── decr.rs │ │ ├── decrby.rs │ │ ├── get.rs │ │ ├── incr.rs │ │ ├── incrby.rs │ │ ├── mget.rs │ │ ├── mod.rs │ │ ├── mset.rs │ │ ├── set.rs │ │ └── strlen.rs │ └── unknown.rs ├── command.rs ├── db.rs ├── frame.rs ├── lib.rs ├── main.rs ├── server.rs └── server_handler.rs └── tests ├── test_cmd.rs └── test_performance.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | linker = "rust-lld" 3 | rustflags = ["-C", "linker-flavor=ld.lld"] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish Docker image 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-rudis-docker: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | - name: Log in to the Container registry 21 | uses: docker/login-action@v3 22 | with: 23 | registry: ${{ env.REGISTRY }} 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | - name: Extract metadata (tags, labels) for Docker 27 | id: meta 28 | uses: docker/metadata-action@v5 29 | with: 30 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 31 | flavor: onlatest=true 32 | - name: Build and push Docker image 33 | uses: docker/build-push-action@v5 34 | with: 35 | context: . 36 | file: ./docker/Dockerfile 37 | push: true 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: "docs" 15 | 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Install Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | 36 | - name: Install pnpm 37 | uses: pnpm/action-setup@v3 38 | with: 39 | version: 8 40 | run_install: false 41 | package_json_file: "docs/package-lock.json" 42 | 43 | - name: Install dependencies 44 | run: pnpm install 45 | 46 | - name: Build 47 | run: pnpm build 48 | 49 | - name: Configure pages 50 | uses: actions/configure-pages@v5 51 | 52 | - name: Upload pages artifact 53 | uses: actions/upload-pages-artifact@v3 54 | with: 55 | path: docs/dist 56 | 57 | - name: Deploy 58 | id: deployment 59 | uses: actions/deploy-pages@v4 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | *.rdb 12 | *.aof -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | ".\\Cargo.toml", 4 | ".\\Cargo.toml", 5 | ".\\Cargo.toml", 6 | ".\\Cargo.toml" 7 | ] 8 | } -------------------------------------------------------------------------------- /COMMAND_PLAN.md: -------------------------------------------------------------------------------- 1 | ## 命令列表 2 | 3 | | Command | Supprt | Appendfile | Test case | Document | 4 | | ------- | ------ | ---------- | --------- |--------- | 5 | | set | ✅ | ✅ | ✅ | ✅ | 6 | | get | ✅ | ⚪ | ✅ | ✅ | 7 | | del | ✅ | ✅ | ✅ | ✅ | 8 | | echo | ✅ | ⚪ | ⚪ | ⛔ | 9 | | flushdb | ✅ | ✅ | ⛔ | ⛔ | 10 | | flushall| ✅ | ✅ | ⛔ | ⛔ | 11 | | dbsize | ✅ | ⚪ | ⛔ | ⛔ | 12 | | auth | ✅ | ⚪ | ⛔ | ⛔ | 13 | | select | ✅ | ✅ | ⛔ | ⛔ | 14 | | llen | ✅ | ⚪ | ✅ | ⛔ | 15 | | exists | ✅ | ⚪ | ✅ | ⛔ | 16 | | expire | ✅ | ✅ | ✅ | ⛔ | 17 | | rename | ✅ | ✅ | ✅ | ⛔ | 18 | | move | ✅ | ✅ | ⛔ | ⛔ | 19 | | lpush | ✅ | ✅ | ✅ | ⛔ | 20 | | rpush | ✅ | ✅ | ✅ | ⛔ | 21 | | append | ✅ | ✅ | ✅ | ⛔ | 22 | | incr | ✅ | ✅ | ⛔ | ⛔ | 23 | | decr | ✅ | ✅ | ⛔ | ⛔ | 24 | | lindex | ✅ | ⚪ | ✅ | ⛔ | 25 | | lpop | ✅ | ✅ | ⛔ | ⛔ | 26 | | rpop | ✅ | ✅ | ⛔ | ⛔ | 27 | | lrange | ✅ | ⚪ | ⛔ | ⛔ | 28 | | ttl | ✅ | ⚪ | ⛔ | ⛔ | 29 | | pttl | ✅ | ⚪ | ⛔ | ⛔ | 30 | | type | ✅ | ⚪ | ⛔ | ⛔ | 31 | | sadd | ✅ | ✅ | ✅ | ⛔ | 32 | | smembers| ✅ | ⚪ | ✅ | ⛔ | 33 | | scard | ✅ | ⚪ | ✅ | ⛔ | 34 | | hmset | ✅ | ✅ | ✅ | ⛔ | 35 | | hget | ✅ | ⚪ | ✅ | ⛔ | 36 | | hdel | ✅ | ✅ | ✅ | ⛔ | 37 | | hexists | ✅ | ⚪ | ✅ | ⛔ | 38 | | hset | ✅ | ✅ | ✅ | ⛔ | 39 | | keys | ✅ | ⚪ | ✅ | ⛔ | 40 | | zadd | ✅ | ✅ | ⛔ | ⛔ | 41 | | zscore | ✅ | ⚪ | ⛔ | ⛔ | 42 | | zcard | ✅ | ⚪ | ⛔ | ⛔ | 43 | | zcount | ✅ | ⚪ | ⛔ | ⛔ | 44 | | pexpire | ✅ | ⚪ | ⛔ | ⛔ | 45 | | mset | ✅ | ✅ | ⛔ | ⛔ | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rudis-server" 3 | version = "0.0.6" 4 | authors = ["就眠儀式"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | bytes = "1.3.0" 9 | regex = "1.5.4" 10 | env_logger = "0.8" 11 | anyhow = "1.0.59" 12 | clap = { version = "4.2.1", features = ["derive"] } 13 | tokio = { version = "1.42.0", features = ["full"] } 14 | redis = "0.21.4" 15 | log = "0.4" -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | WORKDIR /rudis 3 | RUN cargo install --git https://github.com/sleeprite/rudis.git 4 | EXPOSE 6379 5 | ENTRYPOINT ["rudis-server"] 6 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Rudis Docker Image 2 | 3 | This is the Dockerfile of the Docker image for Rudis. 4 | 5 | ## Image Variants 6 | 7 | ### `ghcr.io/sleeprite/rudis:latest` 8 | 9 | This is the latest released rusdis Docker image. 10 | 11 | ### `ghcr.io/sleeprite/rudis:` 12 | 13 | Rudis Docker image will be builded on each release, [view the package page](https://github.com/sleeprite/rudis/pkgs/container/rudis). 14 | 15 | ## How to use this image 16 | 17 | ### Base 18 | 19 | ```sh 20 | docker run -p 6379:6379 ghcr.io/sleeprite/rudis:latest 21 | ``` 22 | 23 | ### With Args 24 | 25 | You can add all supported args at the end, like 26 | 27 | ```sh 28 | docker run -p 6379:8848 ghcr.io/sleeprite/rudis:latest --port 8848 29 | ``` 30 | 31 | ### Handle Data 32 | 33 | Rudis Docker image's default `WORKDIR` is /rudis, but you can change it with arg `--dir /some/other/path` 34 | 35 | So bind /rudis to handle data 36 | 37 | ```sh 38 | docker run -p 6379:6379 -v /some/path/to/save/data:/rudis ghcr.io/sleeprite/rudis:latest --save 60/1 39 | ``` 40 | 41 | You can use a config file like this 42 | 43 | ```sh 44 | touch ./config.properties 45 | docker run -p 6379:6379 -v ./:/rudis ghcr.io/sleeprite/rudis:latest --config config.properties 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | dist/ 4 | node_modules/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /docs/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "markdownlint/style/prettier" 3 | } 4 | -------------------------------------------------------------------------------- /docs/.markdownlintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | README.md 3 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*eslint-plugin-* 2 | public-hoist-pattern[]=@typescript-eslint/eslint-plugin 3 | public-hoist-pattern[]=@antfu/* -------------------------------------------------------------------------------- /docs/.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /docs/.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['stylelint-stylus'], 3 | rules: { 4 | 'stylus/pythonic': 'never', 5 | 'stylus/declaration-colon': 'always', 6 | 'stylus/semicolon': 'never', 7 | 'stylus/single-line-comment-double-slash-space-after': 'always', 8 | 'stylus/property-no-unknown': null, 9 | 'stylus/selector-type-no-unknown': null, 10 | 'stylus/selector-list-comma': 'always', 11 | 'stylus/indentation': [ 12 | 2, 13 | { 14 | indentInsideParens: 'twice', 15 | }, 16 | ], 17 | 'rule-empty-line-before': [ 18 | 'always', 19 | { 20 | except: ['first-nested'], 21 | }, 22 | ], 23 | }, 24 | extends: ['stylelint-stylus/standard'], 25 | } 26 | -------------------------------------------------------------------------------- /docs/.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": false, 3 | "brackets": "always", 4 | "colons": "always", 5 | "colors": "always", 6 | "commaSpace": "always", 7 | "commentSpace": "always", 8 | "cssLiteral": "never", 9 | "customProperties": [], 10 | "depthLimit": false, 11 | "duplicates": true, 12 | "efficient": "always", 13 | "exclude": [], 14 | "extendPref": "@extends", 15 | "globalDupe": true, 16 | "groupOutputByFile": true, 17 | "indentPref": false, 18 | "leadingZero": "always", 19 | "maxErrors": false, 20 | "maxWarnings": false, 21 | "mixed": true, 22 | "mixins": [], 23 | "namingConvention": false, 24 | "namingConventionStrict": false, 25 | "none": "always", 26 | "noImportant": false, 27 | "parenSpace": "never", 28 | "placeholders": "always", 29 | "prefixVarsWithDollar": "always", 30 | "quotePref": "double", 31 | "reporterOptions": { 32 | "columns": ["lineData", "severity", "description", "rule"], 33 | "columnSplitter": " ", 34 | "showHeaders": false, 35 | "truncate": true 36 | }, 37 | "semicolons": "never", 38 | "sortOrder": ["grouped", "alphabetical"], 39 | "stackedProperties": "never", 40 | "trailingWhitespace": "never", 41 | "universal": false, 42 | "valid": true, 43 | "zeroUnits": "never", 44 | "zIndexNormalize": false, 45 | "stylusSupremacy.selectorSeparator": ",\n", 46 | "stylusSupremacy.insertNewLineAroundBlocks": true 47 | } 48 | -------------------------------------------------------------------------------- /docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import { FlatCompat } from '@eslint/eslintrc' 3 | 4 | const compat = new FlatCompat() 5 | 6 | export default antfu({ 7 | ignores: [ 8 | '*.sh', 9 | '*.md', 10 | '*.woff', 11 | '*.ttf', 12 | '.vscode/**', 13 | '.idea/**', 14 | '.husky/**', 15 | '.local/**', 16 | 'dist/**', 17 | 'node_modules/**', 18 | '!docs/.vitepress/**', 19 | 'docs/.vitepress/cache/**', 20 | '.netlify/**', 21 | ], 22 | 23 | typescript: true, 24 | vue: true, 25 | 26 | ...compat.config({ 27 | rules: { 28 | 'comma-dangle': ['error', 'only-multiline'], 29 | 'quotes': 'off', 30 | 'no-tabs': 'off', 31 | 'arrow-parens': ['error', 'always'], 32 | '@typescript-eslint/quotes': [ 33 | 'error', 34 | 'double', 35 | { avoidEscape: true }, 36 | ], 37 | 'indent': 'off', 38 | 'semi': ['error', 'never'], 39 | '@typescript-eslint/indent': ['error', 'tab'], 40 | '@typescript-eslint/brace-style': ['error', '1tbs'], 41 | '@typescript-eslint/semi': ['error', 'never'], 42 | 'vue/no-extra-parens': 'off', 43 | 'vue/html-indent': ['error', 'tab'], 44 | 'curly': ['error', 'all'], 45 | 'brace-style': ['error', '1tbs'], 46 | 'no-console': 'off', 47 | 'no-debugger': 'off', 48 | 'vue/multi-word-component-names': 'off', 49 | 'vue/comment-directive': 'off', 50 | 'no-unused-vars': 'off', 51 | 'vue/no-parsing-error': [ 52 | 2, 53 | { 54 | 'x-invalid-end-tag': false, 55 | 'missing-semicolon-after-character-reference': false, 56 | }, 57 | ], 58 | 59 | /* --ECMAScript 6 ES6-- */ 60 | 'no-useless-escape': 'off', 61 | 'no-unused-expressions': [ 62 | 'error', 63 | { allowShortCircuit: true, allowTernary: true }, 64 | ], 65 | }, 66 | }), 67 | }) 68 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-docs", 3 | "type": "module", 4 | "version": "3.0.0", 5 | "private": true, 6 | "description": "Docs for the Rust.", 7 | "engines": { 8 | "node": ">=20", 9 | "pnpm": ">=8" 10 | }, 11 | "scripts": { 12 | "preinstall": "npx only-allow pnpm", 13 | "test": "pnpm lint && pnpm build", 14 | "dev": "vitepress dev src", 15 | "build": "vitepress build src", 16 | "preview": "vitepress preview src", 17 | "lint": "pnpm lint:es && pnpm lint:mdl && pnpm lint:style", 18 | "lint:fix": "pnpm lint:es:fix && pnpm lint:style:fix", 19 | "lint:es": "eslint . ", 20 | "lint:es:fix": "eslint . --fix", 21 | "lint:mdl": "markdownlint \"**/*.md\" \".github/**/*.md\" --enable sentences-per-line --disable MD025 MD033", 22 | "lint:style": "stylelint \"**/*.{styl,vue}\" \"src/.vitepress/**/*.{styl,vue}\"", 23 | "lint:style:fix": "stylelint --fix \"**/*.{styl,vue}\" \"src/.vitepress/**/*.{styl,vue}\"" 24 | }, 25 | "dependencies": { 26 | "@iconify-prerendered/vue-mdi": "0.23.1702456985", 27 | "@tanstack/vue-query": "5.17.8", 28 | "@vueuse/core": "10.7.1", 29 | "axios": "1.6.5", 30 | "element-plus": "2.4.4", 31 | "lodash.groupby": "4.6.0", 32 | "markdown-it": "14.0.0", 33 | "markdown-it-shortcode-tag": "1.1.0", 34 | "moment": "2.30.1", 35 | "@octokit/types": "12.4.0", 36 | "@octokit/rest": "20.0.2", 37 | "node-fetch": "3.3.2" 38 | }, 39 | "devDependencies": { 40 | "@antfu/eslint-config": "^2.6.1", 41 | "@eslint/eslintrc": "^3.0.0", 42 | "@mdit/plugin-attrs": "0.8.0", 43 | "@mdit/plugin-figure": "0.8.0", 44 | "@mdit/plugin-img-lazyload": "0.8.0", 45 | "@mdit/plugin-img-mark": "0.8.0", 46 | "@mdit/plugin-img-size": "0.8.0", 47 | "@mdit/plugin-include": "0.8.0", 48 | "@resvg/resvg-js": "2.6.0", 49 | "@types/gtag.js": "0.0.18", 50 | "@types/lodash.groupby": "4.6.9", 51 | "@types/markdown-it": "13.0.7", 52 | "@types/node": "20.10.7", 53 | "@typescript-eslint/eslint-plugin": "6.18.0", 54 | "@typescript-eslint/parser": "6.18.0", 55 | "eslint": "8.56.0", 56 | "eslint-config-standard": "17.1.0", 57 | "eslint-plugin-vue": "9.19.2", 58 | "feed": "4.2.2", 59 | "lint-staged": "15.2.0", 60 | "markdownlint": "0.32.1", 61 | "markdownlint-cli": "0.38.0", 62 | "naive-ui": "^2.38.2", 63 | "sentences-per-line": "0.2.1", 64 | "stylelint": "16.1.0", 65 | "stylelint-stylus": "1.0.0", 66 | "stylus": "0.62.0", 67 | "unplugin-element-plus": "0.8.0", 68 | "vite-plugin-eslint": "1.8.1", 69 | "vitepress": "1.0.0-rc.35", 70 | "vitepress-plugin-tabs": "0.5.0", 71 | "vue": "3.4.5", 72 | "vue-eslint-parser": "9.3.2", 73 | "x-satori": "0.1.5" 74 | }, 75 | "simple-git-hooks": { 76 | "pre-commit": "pnpm lint-staged" 77 | }, 78 | "lint-staged": { 79 | "*.{styl,vue}": "stylelint --fix", 80 | "*.{html,json}": "prettier --write" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/chunk-HL2QZUHZ.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __commonJS = (cb, mod) => function __require() { 8 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 9 | }; 10 | var __export = (target, all) => { 11 | for (var name in all) 12 | __defProp(target, name, { get: all[name], enumerable: true }); 13 | }; 14 | var __copyProps = (to, from, except, desc) => { 15 | if (from && typeof from === "object" || typeof from === "function") { 16 | for (let key of __getOwnPropNames(from)) 17 | if (!__hasOwnProp.call(to, key) && key !== except) 18 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 19 | } 20 | return to; 21 | }; 22 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( 23 | // If the importer is in node compatibility mode or this is not an ESM 24 | // file that has been converted to a CommonJS file using a Babel- 25 | // compatible transform (i.e. "__esModule" has not been set), then set 26 | // "default" to the CommonJS "module.exports" for node compatibility. 27 | isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, 28 | mod 29 | )); 30 | var __accessCheck = (obj, member, msg) => { 31 | if (!member.has(obj)) 32 | throw TypeError("Cannot " + msg); 33 | }; 34 | var __privateGet = (obj, member, getter) => { 35 | __accessCheck(obj, member, "read from private field"); 36 | return getter ? getter.call(obj) : member.get(obj); 37 | }; 38 | var __privateAdd = (obj, member, value) => { 39 | if (member.has(obj)) 40 | throw TypeError("Cannot add the same private member more than once"); 41 | member instanceof WeakSet ? member.add(obj) : member.set(obj, value); 42 | }; 43 | var __privateSet = (obj, member, value, setter) => { 44 | __accessCheck(obj, member, "write to private field"); 45 | setter ? setter.call(obj, value) : member.set(obj, value); 46 | return value; 47 | }; 48 | var __privateWrapper = (obj, member, setter, getter) => ({ 49 | set _(value) { 50 | __privateSet(obj, member, value, setter); 51 | }, 52 | get _() { 53 | return __privateGet(obj, member, getter); 54 | } 55 | }); 56 | var __privateMethod = (obj, member, method) => { 57 | __accessCheck(obj, member, "access private method"); 58 | return method; 59 | }; 60 | 61 | export { 62 | __commonJS, 63 | __export, 64 | __toESM, 65 | __privateGet, 66 | __privateAdd, 67 | __privateSet, 68 | __privateWrapper, 69 | __privateMethod 70 | }; 71 | //# sourceMappingURL=chunk-HL2QZUHZ.js.map 72 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/chunk-HL2QZUHZ.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/chunk-I3RIQLCS.js: -------------------------------------------------------------------------------- 1 | // node_modules/.pnpm/vue-demi@0.14.6_vue@3.4.5/node_modules/vue-demi/lib/index.mjs 2 | var isVue2 = false; 3 | var isVue3 = true; 4 | function set(target, key, val) { 5 | if (Array.isArray(target)) { 6 | target.length = Math.max(target.length, key); 7 | target.splice(key, 1, val); 8 | return val; 9 | } 10 | target[key] = val; 11 | return val; 12 | } 13 | function del(target, key) { 14 | if (Array.isArray(target)) { 15 | target.splice(key, 1); 16 | return; 17 | } 18 | delete target[key]; 19 | } 20 | 21 | export { 22 | isVue2, 23 | isVue3, 24 | set, 25 | del 26 | }; 27 | //# sourceMappingURL=chunk-I3RIQLCS.js.map 28 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/chunk-I3RIQLCS.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../../../node_modules/.pnpm/vue-demi@0.14.6_vue@3.4.5/node_modules/vue-demi/lib/index.mjs"], 4 | "sourcesContent": ["import * as Vue from 'vue'\n\nvar isVue2 = false\nvar isVue3 = true\nvar Vue2 = undefined\n\nfunction install() {}\n\nexport function set(target, key, val) {\n if (Array.isArray(target)) {\n target.length = Math.max(target.length, key)\n target.splice(key, 1, val)\n return val\n }\n target[key] = val\n return val\n}\n\nexport function del(target, key) {\n if (Array.isArray(target)) {\n target.splice(key, 1)\n return\n }\n delete target[key]\n}\n\nexport * from 'vue'\nexport {\n Vue,\n Vue2,\n isVue2,\n isVue3,\n install,\n}\n"], 5 | "mappings": ";AAEA,IAAI,SAAS;AACb,IAAI,SAAS;AAKN,SAAS,IAAI,QAAQ,KAAK,KAAK;AACpC,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,SAAS,KAAK,IAAI,OAAO,QAAQ,GAAG;AAC3C,WAAO,OAAO,KAAK,GAAG,GAAG;AACzB,WAAO;AAAA,EACT;AACA,SAAO,GAAG,IAAI;AACd,SAAO;AACT;AAEO,SAAS,IAAI,QAAQ,KAAK;AAC/B,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO,KAAK,CAAC;AACpB;AAAA,EACF;AACA,SAAO,OAAO,GAAG;AACnB;", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/element-plus_es_components_loading_style_css.js: -------------------------------------------------------------------------------- 1 | // node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/es/components/base/style/css.mjs 2 | import "D:/Projects/开源/codecrafters-redis-rust/docs/node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/theme-chalk/base.css"; 3 | 4 | // node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/es/components/loading/style/css.mjs 5 | import "D:/Projects/开源/codecrafters-redis-rust/docs/node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/theme-chalk/el-loading.css"; 6 | //# sourceMappingURL=element-plus_es_components_loading_style_css.js.map 7 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/element-plus_es_components_loading_style_css.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../../../node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/es/components/base/style/css.mjs", "../../../../node_modules/.pnpm/element-plus@2.4.4_vue@3.4.5/node_modules/element-plus/es/components/loading/style/css.mjs"], 4 | "sourcesContent": ["import 'element-plus/theme-chalk/base.css';\n//# sourceMappingURL=css.mjs.map\n", "import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-loading.css';\n//# sourceMappingURL=css.mjs.map\n"], 5 | "mappings": ";AAAA,OAAO;;;ACCP,OAAO;", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/vitepress-plugin-tabs_client.js: -------------------------------------------------------------------------------- 1 | import { 2 | reactive, 3 | watch 4 | } from "./chunk-T3FA6UVC.js"; 5 | import "./chunk-HL2QZUHZ.js"; 6 | 7 | // node_modules/.pnpm/vitepress-plugin-tabs@0.5.0_vitepress@1.0.0-rc.35_vue@3.4.5/node_modules/vitepress-plugin-tabs/src/client/index.ts 8 | import PluginTabs from "D:/Projects/开源/codecrafters-redis-rust/docs/node_modules/.pnpm/vitepress-plugin-tabs@0.5.0_vitepress@1.0.0-rc.35_vue@3.4.5/node_modules/vitepress-plugin-tabs/src/client/PluginTabs.vue"; 9 | import PluginTabsTab from "D:/Projects/开源/codecrafters-redis-rust/docs/node_modules/.pnpm/vitepress-plugin-tabs@0.5.0_vitepress@1.0.0-rc.35_vue@3.4.5/node_modules/vitepress-plugin-tabs/src/client/PluginTabsTab.vue"; 10 | 11 | // node_modules/.pnpm/vitepress-plugin-tabs@0.5.0_vitepress@1.0.0-rc.35_vue@3.4.5/node_modules/vitepress-plugin-tabs/src/client/useTabsSelectedState.ts 12 | var injectionKey = "vitepress:tabSharedState"; 13 | var ls = typeof localStorage !== "undefined" ? localStorage : null; 14 | var localStorageKey = "vitepress:tabsSharedState"; 15 | var setLocalStorageValue = (v) => { 16 | if (!ls) 17 | return; 18 | ls.setItem(localStorageKey, JSON.stringify(v)); 19 | }; 20 | var provideTabsSharedState = (app) => { 21 | const state = reactive({}); 22 | watch( 23 | () => state.content, 24 | (newStateContent, oldStateContent) => { 25 | if (newStateContent && oldStateContent) { 26 | setLocalStorageValue(newStateContent); 27 | } 28 | }, 29 | { deep: true } 30 | ); 31 | app.provide(injectionKey, state); 32 | }; 33 | 34 | // node_modules/.pnpm/vitepress-plugin-tabs@0.5.0_vitepress@1.0.0-rc.35_vue@3.4.5/node_modules/vitepress-plugin-tabs/src/client/index.ts 35 | var enhanceAppWithTabs = (app) => { 36 | provideTabsSharedState(app); 37 | app.component("PluginTabs", PluginTabs); 38 | app.component("PluginTabsTab", PluginTabsTab); 39 | }; 40 | export { 41 | enhanceAppWithTabs 42 | }; 43 | //# sourceMappingURL=vitepress-plugin-tabs_client.js.map 44 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/vitepress___@vue_devtools-api.js: -------------------------------------------------------------------------------- 1 | import { 2 | isPerformanceSupported, 3 | now, 4 | setupDevtoolsPlugin 5 | } from "./chunk-Y2XQL4KM.js"; 6 | import "./chunk-HL2QZUHZ.js"; 7 | export { 8 | isPerformanceSupported, 9 | now, 10 | setupDevtoolsPlugin 11 | }; 12 | //# sourceMappingURL=vitepress___@vue_devtools-api.js.map 13 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { URL, fileURLToPath } from 'node:url' 3 | import { defineConfig, loadEnv } from 'vitepress' 4 | import ElementPlus from 'unplugin-element-plus/vite' 5 | 6 | import markdownConfig from './config/markdownConfig' 7 | 8 | // For use with loading Markdown plugins 9 | import themeConfig from './config/themeConfig' 10 | 11 | // Theme related config 12 | import headConfig from './config/headConfig' 13 | 14 | // Provides how to generate Meta head tag 15 | 16 | const title = 'Rudis' 17 | const description = 'Discover and read manga, webtoons, comics, and more – easier than ever on your Android device.' 18 | 19 | const env = loadEnv('', process.cwd()) 20 | const hostname: string = env.VITE_HOSTNAME || 'http://localhost:4173' 21 | 22 | export default defineConfig({ 23 | base: "/rudis/", 24 | outDir: '../dist', 25 | title, 26 | lang: "en", 27 | description, 28 | locales: { 29 | root: { 30 | label: 'English', 31 | lang: 'en', 32 | }, 33 | zh: { 34 | label: '简体中文', 35 | lang: 'zh', 36 | link: "/zh/", 37 | themeConfig: { 38 | nav: [{ 39 | text: 'Docs', 40 | link: '/zh/docs/guides/introduce', 41 | activeMatch: '/zh/docs/', 42 | }], 43 | } 44 | }, 45 | }, 46 | sitemap: { 47 | hostname, 48 | }, 49 | cleanUrls: true, 50 | lastUpdated: true, 51 | head: headConfig, 52 | markdown: markdownConfig, 53 | themeConfig, 54 | vite: { 55 | resolve: { 56 | alias: [ 57 | { 58 | find: /^.*VPSwitchAppearance\.vue$/, 59 | replacement: fileURLToPath( 60 | new URL('./theme/components/CustomSwitchAppearance.vue', import.meta.url), 61 | ), 62 | }, 63 | ], 64 | }, 65 | plugins: [ElementPlus({})], 66 | ssr: { 67 | noExternal: ['element-plus'], 68 | }, 69 | }, 70 | }) -------------------------------------------------------------------------------- /docs/src/.vitepress/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_EXTENSION_JSON = 'https://raw.githubusercontent.com/tachiyomiorg/extensions/repo/index.json' 2 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config/headConfig.ts: -------------------------------------------------------------------------------- 1 | import type { HeadConfig } from 'vitepress' 2 | 3 | const headConfig: HeadConfig[] = [ 4 | ['meta', { name: 'darkreader-lock' }], 5 | ['meta', { name: 'theme-color', content: '#818CF8' }], 6 | ['meta', { name: 'msapplication-TileColor', content: '#818CF8' }], 7 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }], 8 | ['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }], 9 | ['link', { rel: 'icon', type: 'image/x-icon', href: '/imgs/favicon.ico' }], 10 | ] 11 | 12 | export default headConfig 13 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config/markdownConfig.ts: -------------------------------------------------------------------------------- 1 | import type { MarkdownOptions } from 'vitepress' 2 | 3 | import { attrs } from '@mdit/plugin-attrs' 4 | import { figure } from '@mdit/plugin-figure' 5 | import { imgLazyload } from '@mdit/plugin-img-lazyload' 6 | import { imgMark } from '@mdit/plugin-img-mark' 7 | import { imgSize } from '@mdit/plugin-img-size' 8 | import { include } from '@mdit/plugin-include' 9 | import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' 10 | import shortcode_plugin from 'markdown-it-shortcode-tag' 11 | import shortcodes from './shortcodes' 12 | 13 | const markdownConfig: MarkdownOptions = { 14 | config: (md) => { 15 | md 16 | .use(attrs) 17 | .use(figure) 18 | .use(imgLazyload) 19 | .use(imgMark) 20 | .use(imgSize) 21 | .use(include, { 22 | currentPath: env => env.filePath, 23 | }) 24 | .use(tabsMarkdownPlugin) 25 | .use(shortcode_plugin, shortcodes) 26 | }, 27 | } 28 | 29 | export default markdownConfig 30 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config/navigation/navbar.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultTheme } from 'vitepress' 2 | 3 | const nav: DefaultTheme.NavItem[] = [ 4 | { 5 | text: 'Docs', 6 | link: '/docs/guides/introduce', 7 | activeMatch: '/docs/', 8 | }, 9 | ] 10 | 11 | export default nav 12 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config/scripts/languages.ts: -------------------------------------------------------------------------------- 1 | export function simpleLangName(code: string) { 2 | if (code === 'all') 3 | return 'All' 4 | 5 | const namesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' }) 6 | return capitalize(namesInEnglish.of(code)!, 'en') 7 | } 8 | 9 | export function langName(code: string) { 10 | if (code === 'all') 11 | return 'All' 12 | 13 | const namesInNative = new Intl.DisplayNames([code], { type: 'language' }) 14 | const namesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' }) 15 | return `${capitalize(namesInEnglish.of(code)!, 'en')} - ${capitalize(namesInNative.of(code)!, code)}` 16 | } 17 | 18 | function capitalize(string: string, locale: string) { 19 | return string.charAt(0).toLocaleUpperCase(locale) + string.substring(1) 20 | } -------------------------------------------------------------------------------- /docs/src/.vitepress/config/themeConfig.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultTheme } from 'vitepress' 2 | 3 | import nav from './navigation/navbar' 4 | import sidebar from './navigation/sidebar' 5 | 6 | const themeConfig: DefaultTheme.Config = { 7 | logo: { 8 | src: '/imgs/logo.png', 9 | height: 24, 10 | width: 24, 11 | }, 12 | nav, 13 | sidebar, 14 | i18nRouting: true, 15 | outline: [2, 3], 16 | socialLinks: [ 17 | { 18 | icon: 'github', 19 | link: 'https://github.com/sleeprite/rudis', 20 | ariaLabel: 'Project GitHub', 21 | }, 22 | { 23 | icon: 'facebook', 24 | link: 'https://facebook.com/tachiyomiorg', 25 | ariaLabel: 'Facebook Page', 26 | }, 27 | ], 28 | editLink: { 29 | pattern: 'https://github.com/tachiyomiorg/website/edit/main/website/src/:path', 30 | text: 'Help us improve this page', 31 | }, 32 | lastUpdated: { 33 | text: 'Last updated', 34 | formatOptions: { 35 | forceLocale: true, 36 | dateStyle: 'long', 37 | timeStyle: 'short', 38 | }, 39 | }, 40 | search: { 41 | provider: 'algolia', 42 | options: { 43 | appId: '2C8EHFTRW7', 44 | apiKey: 'ee38c6e04295e4d206399ab59a58ea9a', 45 | indexName: 'tachiyomi', 46 | }, 47 | }, 48 | } 49 | 50 | export default themeConfig 51 | -------------------------------------------------------------------------------- /docs/src/.vitepress/fonts/Inter-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/.vitepress/fonts/Inter-Bold.otf -------------------------------------------------------------------------------- /docs/src/.vitepress/fonts/Inter-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/.vitepress/fonts/Inter-Medium.otf -------------------------------------------------------------------------------- /docs/src/.vitepress/fonts/Inter-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/.vitepress/fonts/Inter-Regular.otf -------------------------------------------------------------------------------- /docs/src/.vitepress/fonts/Inter-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/.vitepress/fonts/Inter-SemiBold.otf -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | 48 | 65 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/ChangelogsList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 56 | 57 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/CommandsCard.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/Contributors.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 77 | 78 | 107 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/CustomSwitchAppearance.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/Extensions/ExtensionFilters.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 69 | 70 | 81 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/Extensions/ExtensionGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 35 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/Extensions/ExtensionList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/ReleaseDate.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/components/RssLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/data/changelogs.data.ts: -------------------------------------------------------------------------------- 1 | import { defineLoader } from 'vitepress' 2 | // @ts-ignore 3 | import { Octokit } from '@octokit/rest' 4 | // @ts-ignore 5 | import type { GetResponseDataTypeFromEndpointMethod } from '@octokit/types' 6 | // @ts-ignore 7 | import fetch from "node-fetch"; 8 | 9 | const octokit = new Octokit({ 10 | request: { 11 | fetch: fetch 12 | } 13 | }) 14 | 15 | type GitHubReleaseList = GetResponseDataTypeFromEndpointMethod 16 | 17 | declare const data: GitHubReleaseList 18 | export { data } 19 | 20 | export default defineLoader({ 21 | async load(): Promise { 22 | const releases = await octokit.paginate(octokit.repos.listReleases, { 23 | owner: 'sleeprite', 24 | repo: 'rudis', 25 | per_page: 100, 26 | }) 27 | return releases 28 | }, 29 | }) -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/data/news.data.ts: -------------------------------------------------------------------------------- 1 | import { createContentLoader } from 'vitepress' 2 | 3 | export interface News { 4 | title: string 5 | description: string 6 | date: string 7 | url: string 8 | } 9 | 10 | declare const data: News[] 11 | export { data } 12 | 13 | export default createContentLoader('news/*.md', { 14 | excerpt: true, 15 | transform(articles) { 16 | return articles 17 | .filter(({ url }) => url !== '/news/') 18 | .map( 19 | ({ frontmatter, url }) => 20 | { 21 | title: frontmatter.title, 22 | description: frontmatter.description, 23 | date: frontmatter.date, 24 | url, 25 | }, 26 | ) 27 | .sort((a, b) => a.date.toString().localeCompare(b.date.toString())) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/data/release.data.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import DefaultTheme from 'vitepress/theme' 3 | 4 | // Import Stylus files 5 | import './styles/base.styl' 6 | 7 | // Import Global plugins 8 | import 'element-plus/theme-chalk/dark/css-vars.css' 9 | import { VueQueryPlugin } from '@tanstack/vue-query' 10 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' 11 | 12 | // Import icon components 13 | import { IconBugReport, IconDownload, IconNewspaperVariant } from '@iconify-prerendered/vue-mdi' 14 | 15 | import analytics from './plugin/analytics' 16 | import Layout from './Layout.vue' 17 | 18 | export default { 19 | extends: DefaultTheme, 20 | enhanceApp({ app }) { 21 | app.use(VueQueryPlugin) 22 | enhanceAppWithTabs(app) 23 | app.component('IconDownload', IconDownload) 24 | app.component('IconNewspaperVariant', IconNewspaperVariant) 25 | app.component('IconBugReport', IconBugReport) 26 | analytics({ id: 'G-2CBXXM1Y86' }) 27 | }, 28 | Layout, 29 | } -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/plugin/analytics.ts: -------------------------------------------------------------------------------- 1 | // Code based on vitepress-plugin-google-analytics. 2 | // Customized as the plugin did not consider the script loading time. 3 | // https://github.com/ZhongxuYang/vitepress-plugin-google-analytics 4 | 5 | function mountGoogleAnalytics(id: string) { 6 | if (('dataLayer' in window && (window as any).gtag) || window.location.hostname === 'localhost') 7 | return 8 | 9 | const analyticsScript = document.createElement('script') 10 | 11 | analyticsScript.addEventListener('load', () => { 12 | // @ts-expect-error Missing types 13 | window.dataLayer = window.dataLayer || [] 14 | function gtag(..._args: any[]) { 15 | // @ts-expect-error Missing types 16 | // eslint-disable-next-line prefer-rest-params 17 | window.dataLayer.push(arguments) 18 | } 19 | }) 20 | 21 | analyticsScript.src = `https://www.googletagmanager.com/gtag/js?id=${id}` 22 | 23 | document.body.appendChild(analyticsScript) 24 | } 25 | 26 | export default function ({ id }: { id: string }) { 27 | // eslint-disable-next-line node/prefer-global/process 28 | if (process.env.NODE_ENV === 'production' && id && typeof window !== 'undefined') 29 | mountGoogleAnalytics(id) 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/queries/useExtensionsRepositoryQuery.ts: -------------------------------------------------------------------------------- 1 | import type { UseQueryOptions } from '@tanstack/vue-query' 2 | import { useQuery } from '@tanstack/vue-query' 3 | import axios from 'axios' 4 | import { GITHUB_EXTENSION_JSON } from '../../config/constants' 5 | 6 | export type ReleaseType = 'stable' | 'preview' 7 | 8 | export interface Extension { 9 | name: string 10 | pkg: string 11 | apk: string 12 | lang: string 13 | code: number 14 | version: string 15 | sources: Source[] 16 | } 17 | 18 | export interface Source { 19 | name: string 20 | lang: string 21 | id: string 22 | baseUrl: string 23 | versionId: number 24 | } 25 | 26 | type UseExtensionsRepositoryQueryOptions = 27 | UseQueryOptions 28 | 29 | export default function useExtensionsRepositoryQuery(options: UseExtensionsRepositoryQueryOptions = {}) { 30 | return useQuery({ 31 | queryKey: ['extensions'], 32 | queryFn: async () => { 33 | const { data } = await axios.get(GITHUB_EXTENSION_JSON) 34 | 35 | return data 36 | }, 37 | initialData: () => [], 38 | refetchOnWindowFocus: false, 39 | ...options, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/styles/forks/lint.styl: -------------------------------------------------------------------------------- 1 | .extension-list { 2 | > div { 3 | &:not(:first-of-type) { 4 | .extensions-total { 5 | display: none 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/styles/tree.styl: -------------------------------------------------------------------------------- 1 | .tree { 2 | border-radius: 8px 3 | margin: 16px 4 | padding: 16px 5 | color: var(--vp-c-text-1) 6 | // background-color: var(--vp-code-block-bg) 7 | font-family: var(--vp-font-family-mono) 8 | font-size: 0.85rem 9 | line-height: 1.5 10 | 11 | & > ul { 12 | margin: 0 13 | } 14 | 15 | li { 16 | & + li { 17 | margin-top: 0 18 | } 19 | } 20 | 21 | span { 22 | &.folder { 23 | &.root, 24 | &.main { 25 | color: var(--vp-c-brand-darker) 26 | } 27 | 28 | &.dynamic { 29 | color: var(--vp-c-brand-darker) 30 | } 31 | } 32 | 33 | &.file.zip { 34 | color: var(--vp-c-brand-darker) !important 35 | } 36 | 37 | &.file { 38 | .file-extension { 39 | color: var(--vp-c-text-2) 40 | } 41 | } 42 | } 43 | 44 | ul { 45 | padding-left: 5px 46 | list-style: none 47 | 48 | li { 49 | position: relative 50 | padding-left: 15px 51 | -webkit-box-sizing: border-box 52 | -moz-box-sizing: border-box 53 | box-sizing: border-box 54 | 55 | &:before { 56 | top: 15px 57 | left: 0 58 | width: 10px 59 | height: 1px 60 | margin: auto 61 | } 62 | 63 | &:after { 64 | top: 0 65 | bottom: 0 66 | left: 0 67 | width: 1px 68 | height: 100% 69 | } 70 | 71 | &:before, 72 | &:after { 73 | position: absolute 74 | content: '' 75 | background-color: var(--vp-c-text-3) 76 | } 77 | 78 | &:last-child { 79 | &:after { 80 | height: 15px 81 | } 82 | } 83 | } 84 | } 85 | 86 | &-icon { 87 | height: 12px 88 | width: 12px 89 | margin-right: 6px 90 | display: inline-block 91 | } 92 | } 93 | 94 | .dark { 95 | .tree { 96 | span { 97 | &.folder, 98 | &.file.zip { 99 | color: var(--vp-c-brand-lighter) !important 100 | } 101 | 102 | &.dynamic { 103 | color: var(--vp-c-brand-lightest) !important 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /docs/src/.vitepress/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { Component } from 'vue' 3 | 4 | const _default: Component 5 | export default _default 6 | } 7 | -------------------------------------------------------------------------------- /docs/src/docs/advance/persistence.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persistence 3 | titleTemplate: Advance 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Persistence 8 | 9 | How Rudis writes data to disk. 10 | 11 | Persistence refers to the writing of data to durable storage, such as a solid-state disk (SSD). Rudis provides a range of persistence options. 12 | 13 | ## RDB 14 | 15 | By default Rudis saves snapshots of the dataset on disk, in a binary file called dump.rdb. You can configure Rudis to have it save the dataset every N seconds if there are at least M changes in the dataset. 16 | 17 | For example, this configuration will make Rudis automatically dump the dataset to disk every 60 seconds: 18 | 19 | ``` 20 | save=60 21 | ``` 22 | 23 | This strategy is known as snapshotting. 24 | 25 | ``` 26 | dbfilename=dump.rdb 27 | ``` 28 | 29 | By default, the data will be retained in the dump.rdb file in the Rudis installation directory, and you can configure and modify the location through dbfilename. 30 | 31 | ## AOF 32 | 33 | The append-only file is an alternative, fully-durable strategy for Rudis. It became available in version 1.0.0. 34 | 35 | You can turn on the AOF in your configuration file: 36 | 37 | ``` 38 | appendonly=true 39 | ``` 40 | 41 | From now on, every time Rudis receives a command that changes the dataset (e.g. SET) it will append it to the AOF. When you restart Rudis it will re-play the AOF to rebuild the state. 42 | 43 | ``` 44 | appendfilename=./data/appendonly.aof 45 | ``` 46 | 47 | The data will be persisted to the appendonly.aof file in the Rudis installation directory by default, and you can configure and modify the location through appendfilename. 48 | -------------------------------------------------------------------------------- /docs/src/docs/advance/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Secure 3 | titleTemplate: Advance 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Security 8 | 9 | Security model and features in Rudis. 10 | 11 | ## Network security 12 | 13 | Access to the Rudis port should be denied to everybody but trusted clients in the network, so the servers running Rudis should be directly accessible only by the computers implementing the application using Rudis. 14 | 15 | In the common case of a single computer directly exposed to the internet, such as a virtualized Linux instance (Linode, EC2, ...), the Rudis port should be firewalled to prevent access from the outside. Clients will still be able to access Rudis using the loopback interface. 16 | 17 | Note that it is possible to bind Rudis to a single interface by adding a line like the following to the rudis.properties file: 18 | 19 | ``` 20 | bind=127.0.0.1 21 | ``` 22 | 23 | Failing to protect the Rudis port from the outside can have a big security impact because of the nature of Rudis. For instance, a single FLUSHALL command can be used by an external attacker to delete the whole data set. 24 | 25 | ## Authentication 26 | 27 | The legacy authentication method is enabled by editing the Rudis.conf file, and providing a database password using the setting. This password is then used by all clients.requirepass 28 | 29 | When the setting is enabled, Rudis will refuse any query by unauthenticated clients. A client can authenticate itself by sending the AUTH command followed by the password.requirepass 30 | 31 | ``` 32 | password=12345 33 | ``` 34 | 35 | The goal of the authentication layer is to optionally provide a layer of redundancy. If firewalling or any other system implemented to protect Rudis from external attackers fail, an external client will still not be able to access the Rudis instance without knowledge of the authentication password. 36 | 37 | Since the AUTH command, like every other Rudis command, is sent unencrypted, it does not protect against an attacker that has enough access to the network to perform eavesdropping. -------------------------------------------------------------------------------- /docs/src/docs/commands/generic.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/docs/commands/generic.md -------------------------------------------------------------------------------- /docs/src/docs/commands/generic/auth.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/docs/commands/generic/auth.md -------------------------------------------------------------------------------- /docs/src/docs/commands/hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hash 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Hash 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/docs/commands/hash/hdel.md: -------------------------------------------------------------------------------- 1 | # HDEL 2 | 3 | The Redis Hdel command is used to delete one or more specified fields from the hash table key, and non-existent fields will be ignored. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | HDEL key field [field ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of successfully deleted fields, excluding ignored fields. -------------------------------------------------------------------------------- /docs/src/docs/commands/hash/hset.md: -------------------------------------------------------------------------------- 1 | # HSET 2 | 3 | The Redis Hset command is used to assign values to fields in a hash table. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | HSET key field value [field value ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | If the field is a newly created field in the hash table and the value is successfully set, return 1. If the field in the hash table already exists and the old value has been overwritten by the new value, return 0. -------------------------------------------------------------------------------- /docs/src/docs/commands/key.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Key 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Key 8 | 9 | The Redis key command is used to manage Redis keys. 10 | 11 | -------------------------------------------------------------------------------- /docs/src/docs/commands/key/del.md: -------------------------------------------------------------------------------- 1 | # DEL 2 | 3 | Removes the specified keys. A key is ignored if it does not exist. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | DEL key [key ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | Integer reply: The number of keys that were removed. -------------------------------------------------------------------------------- /docs/src/docs/commands/key/rename.md: -------------------------------------------------------------------------------- 1 | # RENAME 2 | 3 | Renames key to newkey. It returns an error when key does not exist. If newkey already exists it is overwritten, when this happens RENAME executes an implicit DEL operation, so if the deleted key contains a very big value it may cause high latency even if RENAME itself is usually a constant-time operation. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | RENAME key newkey 9 | ``` 10 | 11 | ## Return 12 | 13 | Simple string reply -------------------------------------------------------------------------------- /docs/src/docs/commands/key/ttl.md: -------------------------------------------------------------------------------- 1 | # TTL 2 | 3 | Like TTL this command returns the remaining time to live of a key that has an expire set, with the sole difference that TTL returns the amount of remaining time in seconds while PTTL returns it in milliseconds. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | TTL key 9 | ``` 10 | 11 | ## Return 12 | 13 | Integer reply: TTL in seconds, or a negative value in order to signal an error. 14 | 15 | - The command returns -2 if the key does not exist. 16 | 17 | - The command returns -1 if the key exists but has no associated expire. -------------------------------------------------------------------------------- /docs/src/docs/commands/key/type.md: -------------------------------------------------------------------------------- 1 | # TYPE 2 | 3 | Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, list, set, zset and hash. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | TYPE key 9 | ``` 10 | 11 | ## Return 12 | 13 | Simple string reply: type of key, or none when key does not exist. -------------------------------------------------------------------------------- /docs/src/docs/commands/list.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: List 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # List 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/docs/commands/list/llen.md: -------------------------------------------------------------------------------- 1 | # LLEN 2 | 3 | The Redis Llen command is used to return the length of a list. If the list key does not exist, it is interpreted as an empty list and returns 0. If the key is not a list type, return an error. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | LLEN key 9 | ``` 10 | 11 | ## Return 12 | 13 | The length of the list. -------------------------------------------------------------------------------- /docs/src/docs/commands/list/lpop.md: -------------------------------------------------------------------------------- 1 | # LPOP 2 | 3 | The Redis Lpop command is used to remove and return the first element of a list. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | LPOP key [count] 9 | ``` 10 | 11 | ## Return 12 | 13 | The first element of the list. When the list key does not exist, return nil. -------------------------------------------------------------------------------- /docs/src/docs/commands/set.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Set 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/docs/commands/set/sadd.md: -------------------------------------------------------------------------------- 1 | # SADD 2 | 3 | The Redis Sadd command adds one or more member elements to a collection, and member elements that already exist in the collection will be ignored. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SADD key member [member ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of deleted keys. -------------------------------------------------------------------------------- /docs/src/docs/commands/set/spop.md: -------------------------------------------------------------------------------- 1 | # SPOP 2 | 3 | The Redis Spop command is used to remove one or more random elements of a specified key from a collection, and after removal, it returns the removed elements. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SPOP key [count] 9 | ``` 10 | 11 | ## Return 12 | 13 | The removed random element. When the set does not exist or is empty, return nil. -------------------------------------------------------------------------------- /docs/src/docs/commands/sortedSet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sorted Set 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Sorted Set 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/docs/commands/sortedSet/zadd.md: -------------------------------------------------------------------------------- 1 | # ZADD 2 | 3 | The Redis Zadd command is used to add one or more member elements and their fractional values to an ordered set. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | ZADD key score member [score member ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of new members successfully added does not include those that have been updated or already exist. -------------------------------------------------------------------------------- /docs/src/docs/commands/string.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: String 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # String 8 | 9 | Essential information to help you get set up with Rudis. 10 | -------------------------------------------------------------------------------- /docs/src/docs/commands/string/get.md: -------------------------------------------------------------------------------- 1 | # GET 2 | 3 | Get the value of. If the key does not exist the special value is returned. An error is returned if the value stored at is not a string, because only handles string values. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | GET key 9 | ``` 10 | 11 | ## Return 12 | 13 | Bulk string reply: the value of key, or nil when key does not exist. -------------------------------------------------------------------------------- /docs/src/docs/commands/string/set.md: -------------------------------------------------------------------------------- 1 | # SET 2 | 3 | The Rudis SET command is used to set the value of a given key. If the key has already stored other values, SET will overwrite the old value and ignore the type. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SET key value [NX | XX] [EX seconds | PX milliseconds ] 9 | ``` 10 | 11 | ## Option 12 | 13 | The SET command supports a set of options that modify its behavior: 14 | 15 | - EX seconds -- Set the specified expire time, in seconds (a positive integer). 16 | - PX milliseconds -- Set the specified expire time, in milliseconds (a positive integer). 17 | - NX -- Only set the key if it does not already exist. 18 | - XX -- Only set the key if it already exists. 19 | 20 | ## Return 21 | 22 | Simple string reply: OK if SET was executed correctly. 23 | 24 | Null reply: (nil) if the SET operation was not performed because the user specified the NX or XX option but the condition was not met. -------------------------------------------------------------------------------- /docs/src/docs/guides/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelogs 3 | description: Changelogs of all Tachiyomi stable releases. 4 | lastUpdated: false 5 | editLink: false 6 | prev: false 7 | next: false 8 | --- 9 | 10 | # Changelogs 11 | 12 | Changelogs of all Rudis stable releases, which are also available [on GitHub](https://github.com/sleeprite/rudis/releases). Preview releases can be seen [on GitHub](https://github.com/sleeprite/rudis/releases). 13 | 14 | --- 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /docs/src/docs/guides/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Install 3 | description: Browse and download sources for Tachiyomi. 4 | lastUpdated: false 5 | editLink: false 6 | prev: false 7 | next: false 8 | --- 9 | 10 | # Install 11 | 12 | Install Rudis on Linux, macOS, and Windows. 13 | 14 | This is a an installation guide. You'll learn how to install, run, and experiment with the Rudis server process. 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/src/docs/guides/introduce.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduce 3 | titleTemplate: Guides 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Introduce 8 | 9 | Rudis is a high-performance in memory database. 10 | 11 | The goal of Rudis is to become a high-performance, reliable, and secure key value storage service, while leveraging the advantages of the Rust language to redesign and implement the core functions of Redis, making it more suitable for the needs of modern applications. 12 | 13 | ## Star History 14 | 15 | [![Star History Chart](https://api.star-history.com/svg?repos=sleeprite/rudis&type=Date)](https://star-history.com/#sleeprite/rudis&Date) -------------------------------------------------------------------------------- /docs/src/docs/guides/protocolSpec.md: -------------------------------------------------------------------------------- 1 | # Protocol Spec 2 | 3 | ## Network layer 4 | 5 | A client connects to a Rudis server by creating a TCP connection to its port (the default is 6379). 6 | 7 | While RESP is technically non-TCP specific, the protocol is used exclusively with TCP connections (or equivalent stream-oriented connections like Unix sockets) in the context of Rudis. 8 | 9 | ## RESP protocol description 10 | 11 | ### Simple strings 12 | 13 | Simple strings are encoded as a plus (+) character, followed by a string. The string mustn't contain a CR (\r) or LF (\n) character and is terminated by CRLF (i.e., \r\n). 14 | 15 | Simple strings transmit short, non-binary strings with minimal overhead. For example, many Rudis commands reply with just "OK" on success. The encoding of this Simple String is the following 5 bytes: 16 | 17 | ``` 18 | +OK\r\n 19 | ``` 20 | 21 | When Rudis replies with a simple string, a client library should return to the caller a string value composed of the first character after the + up to the end of the string, excluding the final CRLF bytes. 22 | 23 | To send binary strings, use bulk strings instead. -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: home 4 | 5 | hero: 6 | name: Rudis 7 | text: A high-performance in memory database 8 | tagline: Remote Dictionary Service,In memory database 9 | image: 10 | light: /imgs/logo.png 11 | dark: /imgs/logo.png 12 | alt: General 13 | actions: 14 | - theme: brand 15 | text: Introduce 16 | link: /docs/guides/introduce 17 | - theme: alt 18 | text: Install 19 | link: /docs/guides/install 20 | 21 | features: 22 | - title: Tracking 23 | details: Automatically keep track of your series with MyAnimeList, AniList, Kitsu, and more. 24 | icon: 25 | link: /docs/guides/tracking 26 | linkText: Setup tracking 27 | - title: Extensions 28 | details: Bring your own content from a variety of sources. 29 | icon: 30 | link: /extensions/ 31 | linkText: Find extensions 32 | - title: Customization 33 | details: Make it yours with multiple reading modes, custom color filters, and many other settings. 34 | icon: 35 | link: /docs/guides/getting-started 36 | linkText: Get started 37 | --- 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/src/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #818CF8 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/src/public/imgs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/public/imgs/favicon.ico -------------------------------------------------------------------------------- /docs/src/public/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/public/imgs/logo.png -------------------------------------------------------------------------------- /docs/src/zh/docs/advance/persistence.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persistence 3 | titleTemplate: Advance 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Persistence 8 | 9 | How Redis writes data to disk. 10 | 11 | Persistence refers to the writing of data to durable storage, such as a solid-state disk (SSD). Redis provides a range of persistence options. 12 | 13 | ## RDB 14 | 15 | By default Redis saves snapshots of the dataset on disk, in a binary file called dump.rdb. You can configure Redis to have it save the dataset every N seconds if there are at least M changes in the dataset. 16 | 17 | For example, this configuration will make Redis automatically dump the dataset to disk every 60 seconds: 18 | 19 | ``` 20 | save=60 21 | ``` 22 | 23 | This strategy is known as snapshotting. 24 | 25 | ``` 26 | dbfilename=dump.rdb 27 | ``` 28 | 29 | By default, the data will be retained in the dump.rdb file in the Rudis installation directory, and you can configure and modify the location through dbfilename. 30 | 31 | ## AOF 32 | 33 | The append-only file is an alternative, fully-durable strategy for Rudis. It became available in version 1.0.0. 34 | 35 | You can turn on the AOF in your configuration file: 36 | 37 | ``` 38 | appendonly=true 39 | ``` 40 | 41 | From now on, every time Rudis receives a command that changes the dataset (e.g. SET) it will append it to the AOF. When you restart Rudis it will re-play the AOF to rebuild the state. 42 | 43 | ``` 44 | appendfilename=./data/appendonly.aof 45 | ``` 46 | 47 | The data will be persisted to the appendonly.aof file in the Rudis installation directory by default, and you can configure and modify the location through appendfilename. 48 | -------------------------------------------------------------------------------- /docs/src/zh/docs/advance/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Secure 3 | titleTemplate: Advance 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Security 8 | 9 | Security model and features in Redis. 10 | 11 | ## Network security 12 | 13 | Access to the Redis port should be denied to everybody but trusted clients in the network, so the servers running Redis should be directly accessible only by the computers implementing the application using Redis. 14 | 15 | In the common case of a single computer directly exposed to the internet, such as a virtualized Linux instance (Linode, EC2, ...), the Redis port should be firewalled to prevent access from the outside. Clients will still be able to access Redis using the loopback interface. 16 | 17 | Note that it is possible to bind Redis to a single interface by adding a line like the following to the rudis.properties file: 18 | 19 | ``` 20 | bind=127.0.0.1 21 | ``` 22 | 23 | Failing to protect the Redis port from the outside can have a big security impact because of the nature of Redis. For instance, a single FLUSHALL command can be used by an external attacker to delete the whole data set. 24 | 25 | ## Authentication 26 | 27 | The legacy authentication method is enabled by editing the redis.conf file, and providing a database password using the setting. This password is then used by all clients.requirepass 28 | 29 | When the setting is enabled, Redis will refuse any query by unauthenticated clients. A client can authenticate itself by sending the AUTH command followed by the password.requirepass 30 | 31 | ``` 32 | password=12345 33 | ``` 34 | 35 | The goal of the authentication layer is to optionally provide a layer of redundancy. If firewalling or any other system implemented to protect Redis from external attackers fail, an external client will still not be able to access the Redis instance without knowledge of the authentication password. 36 | 37 | Since the AUTH command, like every other Redis command, is sent unencrypted, it does not protect against an attacker that has enough access to the network to perform eavesdropping. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/generic.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/zh/docs/commands/generic.md -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/generic/auth.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/docs/src/zh/docs/commands/generic/auth.md -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hash 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Hash 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/hash/hdel.md: -------------------------------------------------------------------------------- 1 | # HDEL 2 | 3 | The Redis Hdel command is used to delete one or more specified fields from the hash table key, and non-existent fields will be ignored. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | HDEL key field [field ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of successfully deleted fields, excluding ignored fields. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/hash/hset.md: -------------------------------------------------------------------------------- 1 | # HSET 2 | 3 | The Redis Hset command is used to assign values to fields in a hash table. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | HSET key field value [field value ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | If the field is a newly created field in the hash table and the value is successfully set, return 1. If the field in the hash table already exists and the old value has been overwritten by the new value, return 0. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/key.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Key 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Key 8 | 9 | The Redis key command is used to manage Redis keys. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/key/del.md: -------------------------------------------------------------------------------- 1 | # DEL 2 | 3 | Removes the specified keys. A key is ignored if it does not exist. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | DEL key [key ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | Integer reply: The number of keys that were removed. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/key/rename.md: -------------------------------------------------------------------------------- 1 | # RENAME 2 | 3 | Renames key to newkey. It returns an error when key does not exist. If newkey already exists it is overwritten, when this happens RENAME executes an implicit DEL operation, so if the deleted key contains a very big value it may cause high latency even if RENAME itself is usually a constant-time operation. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | RENAME key newkey 9 | ``` 10 | 11 | ## Return 12 | 13 | Simple string reply -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/key/ttl.md: -------------------------------------------------------------------------------- 1 | # TTL 2 | 3 | Like TTL this command returns the remaining time to live of a key that has an expire set, with the sole difference that TTL returns the amount of remaining time in seconds while PTTL returns it in milliseconds. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | TTL key 9 | ``` 10 | 11 | ## Return 12 | 13 | Integer reply: TTL in seconds, or a negative value in order to signal an error. 14 | 15 | - The command returns -2 if the key does not exist. 16 | 17 | - The command returns -1 if the key exists but has no associated expire. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/key/type.md: -------------------------------------------------------------------------------- 1 | # TYPE 2 | 3 | Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, list, set, zset and hash. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | TYPE key 9 | ``` 10 | 11 | ## Return 12 | 13 | Simple string reply: type of key, or none when key does not exist. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/list.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: List 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # List 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/list/llen.md: -------------------------------------------------------------------------------- 1 | # LLEN 2 | 3 | The Redis Llen command is used to return the length of a list. If the list key does not exist, it is interpreted as an empty list and returns 0. If the key is not a list type, return an error. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | LLEN key 9 | ``` 10 | 11 | ## Return 12 | 13 | The length of the list. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/list/lpop.md: -------------------------------------------------------------------------------- 1 | # LPOP 2 | 3 | The Redis Lpop command is used to remove and return the first element of a list. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | LPOP key [count] 9 | ``` 10 | 11 | ## Return 12 | 13 | The first element of the list. When the list key does not exist, return nil. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/set.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Set 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/set/sadd.md: -------------------------------------------------------------------------------- 1 | # SADD 2 | 3 | The Redis Sadd command adds one or more member elements to a collection, and member elements that already exist in the collection will be ignored. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SADD key member [member ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of deleted keys. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/set/spop.md: -------------------------------------------------------------------------------- 1 | # SPOP 2 | 3 | The Redis Spop command is used to remove one or more random elements of a specified key from a collection, and after removal, it returns the removed elements. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SPOP key [count] 9 | ``` 10 | 11 | ## Return 12 | 13 | The removed random element. When the set does not exist or is empty, return nil. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/sortedSet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sorted Set 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Sorted Set 8 | 9 | Essential information to help you get set up with Rudis. 10 | 11 | ## Installation guide -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/sortedSet/zadd.md: -------------------------------------------------------------------------------- 1 | # ZADD 2 | 3 | The Redis Zadd command is used to add one or more member elements and their fractional values to an ordered set. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | ZADD key score member [score member ...] 9 | ``` 10 | 11 | ## Return 12 | 13 | The number of new members successfully added does not include those that have been updated or already exist. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/string.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: String 3 | titleTemplate: Commands 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # String 8 | 9 | Essential information to help you get set up with Rudis. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/string/get.md: -------------------------------------------------------------------------------- 1 | # GET 2 | 3 | Get the value of. If the key does not exist the special value is returned. An error is returned if the value stored at is not a string, because only handles string values. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | GET key 9 | ``` 10 | 11 | ## Return 12 | 13 | Bulk string reply: the value of key, or nil when key does not exist. -------------------------------------------------------------------------------- /docs/src/zh/docs/commands/string/set.md: -------------------------------------------------------------------------------- 1 | # SET 2 | 3 | The Rudis SET command is used to set the value of a given key. If the key has already stored other values, SET will overwrite the old value and ignore the type. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | SET key value [NX | XX] [EX seconds | PX milliseconds ] 9 | ``` 10 | 11 | ## Option 12 | 13 | The SET command supports a set of options that modify its behavior: 14 | 15 | - EX seconds -- Set the specified expire time, in seconds (a positive integer). 16 | - PX milliseconds -- Set the specified expire time, in milliseconds (a positive integer). 17 | - NX -- Only set the key if it does not already exist. 18 | - XX -- Only set the key if it already exists. 19 | 20 | ## Return 21 | 22 | Simple string reply: OK if SET was executed correctly. 23 | 24 | Null reply: (nil) if the SET operation was not performed because the user specified the NX or XX option but the condition was not met. -------------------------------------------------------------------------------- /docs/src/zh/docs/guides/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelogs 3 | description: Changelogs of all Tachiyomi stable releases. 4 | lastUpdated: false 5 | editLink: false 6 | prev: false 7 | next: false 8 | --- 9 | 10 | # Changelogs 11 | 12 | Changelogs of all Rudis stable releases, which are also available [on GitHub](https://github.com/sleeprite/rudis/releases). Preview releases can be seen [on GitHub](https://github.com/sleeprite/rudis/releases). 13 | 14 | --- 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /docs/src/zh/docs/guides/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Install 3 | description: Browse and download sources for Tachiyomi. 4 | lastUpdated: false 5 | editLink: false 6 | prev: false 7 | next: false 8 | --- 9 | 10 | # Install 11 | 12 | Install Redis on Linux, macOS, and Windows. 13 | 14 | This is a an installation guide. You'll learn how to install, run, and experiment with the Redis server process. 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/src/zh/docs/guides/introduce.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduce 3 | titleTemplate: Guides 4 | description: Essential information to help you get set up with Tachiyomi. 5 | --- 6 | 7 | # Introduce 8 | 9 | Rudis is a high-performance in memory database. 10 | 11 | The goal of Rudis is to become a high-performance, reliable, and secure key value storage service, while leveraging the advantages of the Rust language to redesign and implement the core functions of Redis, making it more suitable for the needs of modern applications. 12 | 13 | ## Star History 14 | 15 | [![Star History Chart](https://api.star-history.com/svg?repos=sleeprite/rudis&type=Date)](https://star-history.com/#sleeprite/rudis&Date) -------------------------------------------------------------------------------- /docs/src/zh/docs/guides/protocolSpec.md: -------------------------------------------------------------------------------- 1 | # Protocol Spec 2 | 3 | ## Network layer 4 | 5 | A client connects to a Redis server by creating a TCP connection to its port (the default is 6379). 6 | 7 | While RESP is technically non-TCP specific, the protocol is used exclusively with TCP connections (or equivalent stream-oriented connections like Unix sockets) in the context of Redis. 8 | 9 | ## RESP protocol description 10 | 11 | ### Simple strings 12 | 13 | Simple strings are encoded as a plus (+) character, followed by a string. The string mustn't contain a CR (\r) or LF (\n) character and is terminated by CRLF (i.e., \r\n). 14 | 15 | Simple strings transmit short, non-binary strings with minimal overhead. For example, many Redis commands reply with just "OK" on success. The encoding of this Simple String is the following 5 bytes: 16 | 17 | ``` 18 | +OK\r\n 19 | ``` 20 | 21 | When Redis replies with a simple string, a client library should return to the caller a string value composed of the first character after the + up to the end of the string, excluding the final CRLF bytes. 22 | 23 | To send binary strings, use bulk strings instead. -------------------------------------------------------------------------------- /docs/src/zh/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: home 4 | 5 | hero: 6 | name: Rudis 7 | text: A high-performance in memory database 8 | tagline: Remote Dictionary Service,In memory database 9 | image: 10 | light: /imgs/logo.png 11 | dark: /imgs/logo.png 12 | alt: General 13 | actions: 14 | - theme: brand 15 | text: Introduce 16 | link: /docs/guides/introduce 17 | - theme: alt 18 | text: Install 19 | link: /docs/guides/install 20 | 21 | features: 22 | - title: Tracking 23 | details: Automatically keep track of your series with MyAnimeList, AniList, Kitsu, and more. 24 | icon: 25 | link: /docs/guides/tracking 26 | linkText: Setup tracking 27 | - title: Extensions 28 | details: Bring your own content from a variety of sources. 29 | icon: 30 | link: /extensions/ 31 | linkText: Find extensions 32 | - title: Customization 33 | details: Make it yours with multiple reading modes, custom color filters, and many other settings. 34 | icon: 35 | link: /docs/guides/getting-started 36 | linkText: Get started 37 | --- 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "useDefineForClassFields": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "paths": { 10 | "@/*": ["./docs/.vitepress/*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "types": ["vite/client", "@types/gtag.js"], 14 | "strict": true, 15 | "sourceMap": true, 16 | "esModuleInterop": true, 17 | "isolatedModules": true, 18 | "skipLibCheck": true 19 | }, 20 | "include": [ 21 | "typings/**/*.d.ts", 22 | "docs/**/*.vue", 23 | "docs/**/*.tsx", 24 | "docs/**/*.ts", 25 | "docs/.**/**/*.ts", 26 | "docs/.**/**/*.tsx", 27 | "docs/.**/**/*.vue" 28 | ], 29 | "exclude": ["**/node_modules/**", "dist/**"], 30 | "extensions": [".js", ".ts", ".tsx", ".jsx", ".vue"], 31 | "allowSyntheticDefaultImports": true 32 | } 33 | -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | r = redis.Redis(host='127.0.0.1', port=6379) 4 | 5 | r.set("username", "username") 6 | result = r.get("username") 7 | 8 | print(result) -------------------------------------------------------------------------------- /examples/flagged/log.csv: -------------------------------------------------------------------------------- 1 | Host,Port,Password,Key,output,flag,username,timestamp 2 | 127.0.0.1,6379,,,"['password', 'username']",,,2024-07-11 12:45:50.519948 3 | 127.0.0.1,6379,,,"['password', 'username']",,,2024-07-11 12:45:52.199694 4 | 127.0.0.1,6379,,,"['password', 'username']",,,2024-07-11 13:52:24.246326 5 | -------------------------------------------------------------------------------- /examples/performance_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/examples/performance_comparison.png -------------------------------------------------------------------------------- /examples/rudis_visualizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/examples/rudis_visualizer.png -------------------------------------------------------------------------------- /examples/rudis_visualizer.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import gradio as gr 3 | 4 | # 连接Redis的函数,接受地址、端口、密码和数据库编号作为参数 5 | def connect_redis(host, port, password, db): 6 | try: 7 | return redis.Redis(host=host, port=int(port), password=password, db=int(db), decode_responses=True) 8 | except redis.ConnectionError as e: 9 | return str(e) 10 | 11 | # 获取所有key的列表 12 | def get_keys(redis_client): 13 | return list(redis_client.keys()) 14 | 15 | # 根据key获取其值 16 | def get_value(redis_client, key): 17 | value = redis_client.get(key) 18 | if value is None: 19 | return f"Key '{key}' not found." 20 | return f"Key: {key}, Value: {value}" 21 | 22 | def main(host, port, password, db, key=None): 23 | try: 24 | redis_client = connect_redis(host, port, password, db) 25 | if key: 26 | return get_value(redis_client, key) 27 | else: 28 | return get_keys(redis_client) 29 | except Exception as e: 30 | return str(e) 31 | 32 | # 创建Gradio界面 33 | iface = gr.Interface( 34 | fn=main, 35 | inputs=[ 36 | gr.Textbox(label="Host", value="127.0.0.1", placeholder="Enter host"), 37 | gr.Number(label="Port", value=6379), 38 | gr.Textbox(label="Password", type="password", placeholder="Enter password"), 39 | gr.Number(label="DB", value=0), 40 | gr.Textbox(label="Key", placeholder="Enter key to get its value") 41 | ], 42 | outputs="text", 43 | title="Rudis Visualizer", 44 | description="Visualize Rudis keys and values." 45 | ) 46 | 47 | # 启动Gradio应用 48 | if __name__ == "__main__": 49 | iface.launch() -------------------------------------------------------------------------------- /logo/logo-padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/logo/logo-padding.png -------------------------------------------------------------------------------- /logo/logo-with-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/logo/logo-with-title.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/logo/logo.png -------------------------------------------------------------------------------- /release/linux/rudis-server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/release/linux/rudis-server -------------------------------------------------------------------------------- /release/linux/rudis-server.properties: -------------------------------------------------------------------------------- 1 | # 绑定端口号 2 | # 指定Rudis服务器监听的TCP端口,客户端通过此端口与Rudis服务器进行通信。 3 | port=6379 4 | 5 | # 数据库数量 6 | # 定义Rudis实例中可用的数据库数量。Rudis默认支持16个独立的数据库。 7 | databases=16 8 | 9 | # 开启命令备份 10 | # 启用AOF(Append Only File)持久化,即所有的写命令都会被记录到一个文件中。 11 | appendonly=true 12 | 13 | # 命令备份文件 14 | # 定义AOF持久化文件的名称。 15 | appendfilename=appendonly.aof 16 | 17 | # 连接上限 18 | # 设置Rudis可以同时处理的最大客户端连接数。 19 | maxclients=1000 20 | 21 | # 备份文件 22 | # 定义RDB(Rudis Database)快照文件的名称,该文件用于数据的持久化存储。 23 | dbfilename=dump.rdb 24 | 25 | # 备份策略,次/n 秒 26 | # 定义RDB快照的自动保存条件,格式为:save 。例如,"save 60 1"表示每60秒如果至少有1个键被改变,则会进行一次快照保存。 27 | save=60/1 20/2 28 | 29 | # 绑定主机 30 | # 指定Rudis服务器绑定的IP地址,可以是IP地址或主机名。127.0.0.1表示只接受来自本机的连接。 31 | bind=127.0.0.1 32 | 33 | # 检测策略,过期检测 34 | # 设置Rudis进行过期键检测的频率,单位为秒。hz=10表示每秒钟进行10次过期键的检测。 35 | hz=10 36 | 37 | # 认证密码 38 | # 启用密码保护,以增强Rudis服务器的安全性。注释掉此行或留空则不启用密码保护。 39 | # password=12345 # 请将12345替换为您希望设置的密码 -------------------------------------------------------------------------------- /release/macOS/rudis-server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/release/macOS/rudis-server -------------------------------------------------------------------------------- /release/macOS/rudis-server.properties: -------------------------------------------------------------------------------- 1 | # 绑定端口号 2 | # 指定Rudis服务器监听的TCP端口,客户端通过此端口与Rudis服务器进行通信。 3 | port=6379 4 | 5 | # 数据库数量 6 | # 定义Rudis实例中可用的数据库数量。Rudis默认支持16个独立的数据库。 7 | databases=16 8 | 9 | # 开启命令备份 10 | # 启用AOF(Append Only File)持久化,即所有的写命令都会被记录到一个文件中。 11 | appendonly=true 12 | 13 | # 命令备份文件 14 | # 定义AOF持久化文件的名称。 15 | appendfilename=appendonly.aof 16 | 17 | # 连接上限 18 | # 设置Rudis可以同时处理的最大客户端连接数。 19 | maxclients=1000 20 | 21 | # 备份文件 22 | # 定义RDB(Rudis Database)快照文件的名称,该文件用于数据的持久化存储。 23 | dbfilename=dump.rdb 24 | 25 | # 备份策略,次/n 秒 26 | # 定义RDB快照的自动保存条件,格式为:save 。例如,"save 60 1"表示每60秒如果至少有1个键被改变,则会进行一次快照保存。 27 | save=60/1 28 | 29 | # 绑定主机 30 | # 指定Rudis服务器绑定的IP地址,可以是IP地址或主机名。127.0.0.1表示只接受来自本机的连接。 31 | bind=127.0.0.1 32 | 33 | # 检测策略,过期检测 34 | # 设置Rudis进行过期键检测的频率,单位为秒。hz=10表示每秒钟进行10次过期键的检测。 35 | hz=10 36 | 37 | # 认证密码 38 | # 启用密码保护,以增强Rudis服务器的安全性。注释掉此行或留空则不启用密码保护。 39 | # password=12345 # 请将12345替换为您希望设置的密码 -------------------------------------------------------------------------------- /release/windows/rudis-server.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeprite/rudis/8aa893caebba65bb0d972d743bb17b75b0e5f5f8/release/windows/rudis-server.exe -------------------------------------------------------------------------------- /release/windows/rudis-server.properties: -------------------------------------------------------------------------------- 1 | # 绑定端口号 2 | # 指定Rudis服务器监听的TCP端口,客户端通过此端口与Rudis服务器进行通信。 3 | port=6379 4 | 5 | # 数据库数量 6 | # 定义Rudis实例中可用的数据库数量。Rudis默认支持16个独立的数据库。 7 | databases=16 8 | 9 | # 开启命令备份 10 | # 启用AOF(Append Only File)持久化,即所有的写命令都会被记录到一个文件中。 11 | appendonly=true 12 | 13 | # 命令备份文件 14 | # 定义AOF持久化文件的名称。 15 | appendfilename=appendonly.aof 16 | 17 | # 连接上限 18 | # 设置Rudis可以同时处理的最大客户端连接数。 19 | maxclients=1000 20 | 21 | # 备份文件 22 | # 定义RDB(Rudis Database)快照文件的名称,该文件用于数据的持久化存储。 23 | dbfilename=dump.rdb 24 | 25 | # 备份策略,次/n 秒 26 | # 定义RDB快照的自动保存条件,格式为:save 。例如,"save 60 1"表示每60秒如果至少有1个键被改变,则会进行一次快照保存。 27 | save=60/1 28 | 29 | # 绑定主机 30 | # 指定Rudis服务器绑定的IP地址,可以是IP地址或主机名。127.0.0.1表示只接受来自本机的连接。 31 | bind=127.0.0.1 32 | 33 | # 检测策略,过期检测 34 | # 设置Rudis进行过期键检测的频率,单位为秒。hz=10表示每秒钟进行10次过期键的检测。 35 | hz=10 36 | 37 | # 认证密码 38 | # 启用密码保护,以增强Rudis服务器的安全性。注释掉此行或留空则不启用密码保护。 39 | # password=12345 # 请将12345替换为您希望设置的密码 -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | #[command(version, author, about, long_about = None)] 5 | pub struct Args { 6 | 7 | /** 8 | * 认证密码 9 | */ 10 | #[arg(short, long)] 11 | pub requirepass: Option, 12 | 13 | /** 14 | * 绑定地址 15 | */ 16 | #[arg(short, long, default_value = "127.0.0.1")] 17 | pub bind: String, 18 | 19 | /** 20 | * 文件路径 21 | */ 22 | // #[arg(short, long, default_value = "dump.rdb")] 23 | // pub dbfilename: String, 24 | 25 | // #[arg(short, long, default_value = "./")] 26 | // pub dir: String, 27 | 28 | /** 29 | * 数据库 30 | */ 31 | #[arg(short, long, default_value = "16")] 32 | pub databases: usize, 33 | 34 | /** 35 | * 监听端口 36 | */ 37 | #[arg(short, long, default_value = "6379")] 38 | pub port: String, 39 | 40 | /** 41 | * 日志级别 42 | */ 43 | #[arg(short, long, default_value = "info")] 44 | pub loglevel: String, 45 | 46 | } -------------------------------------------------------------------------------- /src/cmd/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{frame::Frame, server_handler::ServerHandler}; 4 | 5 | pub struct Auth { 6 | requirepass: String, 7 | } 8 | 9 | impl Auth { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let requirepass = frame.get_arg(1); 13 | 14 | if requirepass.is_none() { 15 | return Err(Error::msg( 16 | "ERR wrong number of arguments for 'auth' command", 17 | )); 18 | } 19 | 20 | Ok(Auth { 21 | requirepass: requirepass.unwrap().to_string(), 22 | }) 23 | } 24 | 25 | pub fn apply(self, handler:&mut ServerHandler) -> Result { 26 | match handler.login(&self.requirepass) { 27 | Ok(_) => Ok(Frame::Ok), 28 | Err(e) => { 29 | Ok(Frame::Error(e.to_string())) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/cmd/dbsize.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Dbsize {} 6 | 7 | impl Dbsize { 8 | 9 | pub fn parse_from_frame(_frame: Frame) -> Result { 10 | Ok(Dbsize {}) 11 | } 12 | 13 | pub fn apply(self, db: &mut Db) -> Result { 14 | let size = db.records.len(); 15 | Ok(Frame::Integer(size as i64)) 16 | } 17 | } -------------------------------------------------------------------------------- /src/cmd/echo.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::frame::Frame; 4 | 5 | pub struct Echo { 6 | str: String, 7 | } 8 | 9 | impl Echo { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let str = match frame.get_arg(1) { 13 | Some(name) => name.to_string(), 14 | None => return Err(Error::msg("ERR wrong number of arguments for 'echo' command")), 15 | }; 16 | Ok(Echo { 17 | str 18 | }) 19 | } 20 | 21 | pub fn apply(self) -> Result { 22 | Ok(Frame::BulkString(self.str)) 23 | } 24 | } -------------------------------------------------------------------------------- /src/cmd/flushall.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Error; 4 | use tokio::sync::oneshot; 5 | use crate::{command::Command, db::{DbGuard, DbMessage}, frame::Frame}; 6 | 7 | use super::flushdb::Flushdb; 8 | 9 | pub struct Flushall {} 10 | 11 | impl Flushall { 12 | 13 | pub fn parse_from_frame(_frame: Frame) -> Result { 14 | Ok(Flushall { }) 15 | } 16 | 17 | pub fn apply(self, db_guard: Arc) -> Result { 18 | let senders = db_guard.get_senders(); 19 | for target_sender in senders { 20 | let (sender, _receiver) = oneshot::channel(); // 创建通道 21 | let _result = target_sender.send(DbMessage { 22 | command: Command::Flushdb(Flushdb {}), 23 | sender: sender 24 | }); 25 | } 26 | Ok(Frame::Ok) 27 | } 28 | } -------------------------------------------------------------------------------- /src/cmd/flushdb.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Flushdb {} 6 | 7 | impl Flushdb { 8 | 9 | pub fn new() -> Flushdb { 10 | Flushdb { } 11 | } 12 | 13 | pub fn parse_from_frame(_frame: Frame) -> Result { 14 | Ok(Flushdb {}) 15 | } 16 | 17 | pub fn apply(self, db: &mut Db) -> Result { 18 | db.expire_records.clear(); 19 | db.records.clear(); 20 | Ok(Frame::Ok) 21 | } 22 | } -------------------------------------------------------------------------------- /src/cmd/hash/hdel.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hdel { 5 | key: String, 6 | fields: Vec, 7 | } 8 | 9 | impl Hdel { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | 14 | if args.len() < 3 { 15 | return Err(Error::msg("ERR wrong number of arguments for 'hdel' command")); 16 | } 17 | 18 | let key = args[1].to_string(); 19 | let fields = args[2..].iter().map(|arg| arg.to_string()).collect(); 20 | 21 | Ok(Hdel { 22 | key, 23 | fields, 24 | }) 25 | } 26 | 27 | pub fn apply(self, db: &mut Db) -> Result { 28 | match db.get_mut(&self.key) { 29 | Some(structure) => { 30 | match structure { 31 | Structure::Hash(hash) => { 32 | 33 | let mut deleted_count = 0; 34 | 35 | for field in &self.fields { 36 | if hash.remove(field).is_some() { 37 | deleted_count += 1; 38 | } 39 | } 40 | 41 | Ok(Frame::Integer(deleted_count as i64)) 42 | }, 43 | _ => { 44 | let f = "ERR Operation against a key holding the wrong kind of value"; 45 | Ok(Frame::Error(f.to_string())) 46 | } 47 | } 48 | }, 49 | None => { 50 | Ok(Frame::Integer(0)) 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/cmd/hash/hexists.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hexists { 5 | key: String, 6 | field: String, 7 | } 8 | 9 | impl Hexists { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let key = frame.get_arg(1); 13 | let field = frame.get_arg(2); 14 | 15 | if key.is_none() || field.is_none() { 16 | return Err(Error::msg("ERR wrong number of arguments for 'hexists' command")); 17 | } 18 | 19 | let final_key = key.unwrap().to_string(); // 键 20 | let final_field = field.unwrap().to_string(); // 字段 21 | 22 | Ok(Hexists { 23 | key: final_key, 24 | field: final_field, 25 | }) 26 | } 27 | 28 | pub fn apply(self, db: &mut Db) -> Result { 29 | match db.get(&self.key) { 30 | Some(structure) => { 31 | match structure { 32 | Structure::Hash(hash) => { 33 | if hash.contains_key(&self.field) { 34 | Ok(Frame::Integer(1)) 35 | } else { 36 | Ok(Frame::Integer(0)) 37 | } 38 | }, 39 | _ => { 40 | let f = "ERR Operation against a key holding the wrong kind of value"; 41 | Ok(Frame::Error(f.to_string())) 42 | } 43 | } 44 | }, 45 | None => Ok(Frame::Integer(0)), 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/hash/hget.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hget { 5 | key: String, 6 | field: String, 7 | } 8 | 9 | impl Hget { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let key = frame.get_arg(1); 14 | let field = frame.get_arg(2); 15 | 16 | if key.is_none() || field.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'hget' command")); 18 | } 19 | 20 | let final_key = key.unwrap().to_string(); // 键 21 | let final_field = field.unwrap().to_string(); // 字段 22 | 23 | Ok(Hget { 24 | key: final_key, 25 | field: final_field, 26 | }) 27 | } 28 | 29 | pub fn apply(self, db: &mut Db) -> Result { 30 | match db.get(&self.key) { 31 | Some(structure) => { 32 | match structure { 33 | Structure::Hash(hash) => { 34 | match hash.get(&self.field) { 35 | Some(value) => Ok(Frame::BulkString(value.clone())), 36 | None => Ok(Frame::Null), 37 | } 38 | }, 39 | _ => { 40 | let f = "ERR Operation against a key holding the wrong kind of value"; 41 | Ok(Frame::Error(f.to_string())) 42 | } 43 | } 44 | }, 45 | None => Ok(Frame::Null), 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/hash/hgetall.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hgetall { 5 | key: String, 6 | } 7 | 8 | impl Hgetall { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let key = frame.get_arg(1); 13 | 14 | if key.is_none() { 15 | return Err(Error::msg("ERR wrong number of arguments for 'hgetall' command")); 16 | } 17 | 18 | let final_key = key.unwrap().to_string(); // 键 19 | 20 | Ok(Hgetall { 21 | key: final_key, 22 | }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::Hash(hash) => { 30 | let mut result = Vec::new(); 31 | for (field, value) in hash.iter() { 32 | result.push(Frame::BulkString(field.clone())); 33 | result.push(Frame::BulkString(value.clone())); 34 | } 35 | Ok(Frame::Array(result)) 36 | }, 37 | _ => { 38 | let f = "ERR Operation against a key holding the wrong kind of value"; 39 | Ok(Frame::Error(f.to_string())) 40 | } 41 | } 42 | }, 43 | None => Ok(Frame::Null), 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/cmd/hash/hkeys.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hkeys { 5 | key: String, 6 | } 7 | 8 | impl Hkeys { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let key = frame.get_arg(1); 13 | 14 | if key.is_none() { 15 | return Err(Error::msg("ERR wrong number of arguments for 'hkeys' command")); 16 | } 17 | 18 | let final_key = key.unwrap().to_string(); // 键 19 | 20 | Ok(Hkeys { 21 | key: final_key, 22 | }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::Hash(hash) => { 30 | let mut keys = Vec::new(); 31 | for key in hash.keys() { 32 | keys.push(Frame::BulkString(key.clone())); 33 | } 34 | Ok(Frame::Array(keys)) 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | Ok(Frame::Error(f.to_string())) 39 | } 40 | } 41 | }, 42 | None => Ok(Frame::Array(vec![])), 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/cmd/hash/hlen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hlen { 5 | key: String, 6 | } 7 | 8 | impl Hlen { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let key = frame.get_arg(1); 13 | 14 | if key.is_none() { 15 | return Err(Error::msg("ERR wrong number of arguments for 'hlen' command")); 16 | } 17 | 18 | let final_key = key.unwrap().to_string(); // 键 19 | 20 | Ok(Hlen { 21 | key: final_key, 22 | }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::Hash(hash) => { 30 | Ok(Frame::Integer(hash.len() as i64)) 31 | }, 32 | _ => { 33 | let f = "ERR Operation against a key holding the wrong kind of value"; 34 | Ok(Frame::Error(f.to_string())) 35 | } 36 | } 37 | }, 38 | None => Ok(Frame::Integer(0)), 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/cmd/hash/hmget.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hmget { 5 | key: String, 6 | fields: Vec, 7 | } 8 | 9 | impl Hmget { 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | 13 | if args.len() < 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'hmget' command")); 15 | } 16 | 17 | let key = args[1].to_string(); 18 | let fields = args[2..].iter().map(|arg| arg.to_string()).collect(); 19 | 20 | Ok(Hmget { 21 | key, 22 | fields, 23 | }) 24 | } 25 | 26 | pub fn apply(self, db: &mut Db) -> Result { 27 | match db.get(&self.key) { 28 | Some(structure) => { 29 | match structure { 30 | Structure::Hash(hash) => { 31 | let mut values = Vec::new(); 32 | for field in &self.fields { 33 | if let Some(value) = hash.get(field) { 34 | values.push(Frame::BulkString(value.clone())); 35 | } else { 36 | values.push(Frame::Null); 37 | } 38 | } 39 | Ok(Frame::Array(values)) 40 | }, 41 | _ => { 42 | let f = "ERR Operation against a key holding the wrong kind of value"; 43 | Ok(Frame::Error(f.to_string())) 44 | } 45 | } 46 | }, 47 | None => { 48 | let null_values = vec![Frame::Null; self.fields.len()]; 49 | Ok(Frame::Array(null_values)) 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/cmd/hash/hmset.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Hmset { 7 | key: String, 8 | fields: HashMap, 9 | } 10 | 11 | impl Hmset { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | 15 | let key = frame.get_arg(1); 16 | 17 | if key.is_none() { 18 | return Err(Error::msg("ERR wrong number of arguments for 'hmset' command")); 19 | } 20 | 21 | let args = frame.get_args(); 22 | 23 | if args.len() % 2 != 0 { 24 | return Err(Error::msg("ERR wrong number of arguments for 'hmset' command")); 25 | } 26 | 27 | let mut fields = HashMap::new(); 28 | 29 | for i in (2..args.len()).step_by(2) { 30 | let field = args[i].to_string(); 31 | let value = args[i + 1].to_string(); 32 | fields.insert(field, value); 33 | } 34 | 35 | Ok(Hmset { 36 | key: key.unwrap().to_string(), 37 | fields, 38 | }) 39 | } 40 | 41 | pub fn apply(self, db: &mut Db) -> Result { 42 | match db.get_mut(&self.key) { 43 | Some(structure) => { 44 | match structure { 45 | Structure::Hash(hash) => { 46 | for (field, value) in self.fields { 47 | hash.insert(field, value); 48 | } 49 | Ok(Frame::SimpleString("OK".to_string())) 50 | }, 51 | _ => { 52 | let f = "ERR Operation against a key holding the wrong kind of value"; 53 | Ok(Frame::Error(f.to_string())) 54 | } 55 | } 56 | }, 57 | None => { 58 | db.insert(self.key, Structure::Hash(self.fields)); 59 | Ok(Frame::SimpleString("OK".to_string())) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/cmd/hash/hset.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Hset { 7 | key: String, 8 | field: String, 9 | value: String, 10 | } 11 | 12 | impl Hset { 13 | 14 | pub fn parse_from_frame(frame: Frame) -> Result { 15 | 16 | let key = frame.get_arg(1); 17 | let field = frame.get_arg(2); 18 | let value = frame.get_arg(3); 19 | 20 | if key.is_none() || field.is_none() || value.is_none() { 21 | return Err(Error::msg("ERR wrong number of arguments for 'hset' command")); 22 | } 23 | 24 | let final_key = key.unwrap().to_string(); 25 | let final_field = field.unwrap().to_string(); 26 | let final_value = value.unwrap().to_string(); 27 | 28 | Ok(Hset { 29 | key: final_key, 30 | field: final_field, 31 | value: final_value, 32 | }) 33 | } 34 | 35 | pub fn apply(self, db: &mut Db) -> Result { 36 | match db.get_mut(&self.key) { 37 | Some(structure) => { 38 | match structure { 39 | Structure::Hash(hash) => { 40 | let field_exists = hash.contains_key(&self.field); 41 | hash.insert(self.field, self.value); 42 | if field_exists { 43 | return Ok(Frame::Integer(0)); 44 | } else { 45 | return Ok(Frame::Integer(1)); 46 | } 47 | }, 48 | _ => { 49 | let f = "ERR Operation against a key holding the wrong kind of value"; 50 | Ok(Frame::Error(f.to_string())) 51 | } 52 | } 53 | }, 54 | None => { 55 | let hash = HashMap::from([(self.field, self.value)]); 56 | db.insert(self.key.clone(), Structure::Hash(hash)); 57 | Ok(Frame::Integer(1)) 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/cmd/hash/hsetnx.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Hsetnx { 7 | key: String, 8 | field: String, 9 | value: String, 10 | } 11 | 12 | impl Hsetnx { 13 | 14 | pub fn parse_from_frame(frame: Frame) -> Result { 15 | 16 | let key = frame.get_arg(1); 17 | let field = frame.get_arg(2); 18 | let value = frame.get_arg(3); 19 | 20 | if key.is_none() || field.is_none() || value.is_none() { 21 | return Err(Error::msg("ERR wrong number of arguments for 'hsetnx' command")); 22 | } 23 | 24 | let final_key = key.unwrap().to_string(); 25 | let final_field = field.unwrap().to_string(); 26 | let final_value = value.unwrap().to_string(); 27 | 28 | Ok(Hsetnx { 29 | key: final_key, 30 | field: final_field, 31 | value: final_value, 32 | }) 33 | } 34 | 35 | pub fn apply(self, db: &mut Db) -> Result { 36 | match db.get_mut(&self.key) { 37 | Some(structure) => { 38 | match structure { 39 | Structure::Hash(hash) => { 40 | if hash.contains_key(&self.field) { 41 | Ok(Frame::Integer(0)) 42 | } else { 43 | hash.insert(self.field, self.value); 44 | Ok(Frame::Integer(1)) 45 | } 46 | }, 47 | _ => { 48 | let f = "ERR Operation against a key holding the wrong kind of value"; 49 | Ok(Frame::Error(f.to_string())) 50 | } 51 | } 52 | }, 53 | None => { 54 | let hash = HashMap::from([(self.field, self.value)]); 55 | db.insert(self.key.clone(), Structure::Hash(hash)); 56 | Ok(Frame::Integer(1)) 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/cmd/hash/hstrlen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hstrlen { 5 | key: String, 6 | field: String, 7 | } 8 | 9 | impl Hstrlen { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let key = frame.get_arg(1); 14 | let field = frame.get_arg(2); 15 | 16 | if key.is_none() || field.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'hstrlen' command")); 18 | } 19 | 20 | let final_key = key.unwrap().to_string(); // 键 21 | let final_field = field.unwrap().to_string(); // 字段 22 | 23 | Ok(Hstrlen { 24 | key: final_key, 25 | field: final_field, 26 | }) 27 | } 28 | 29 | pub fn apply(self, db: &mut Db) -> Result { 30 | match db.get(&self.key) { 31 | Some(structure) => { 32 | match structure { 33 | Structure::Hash(hash) => { 34 | match hash.get(&self.field) { 35 | Some(value) => Ok(Frame::Integer(value.len() as i64)), 36 | None => Ok(Frame::Integer(0)), 37 | } 38 | }, 39 | _ => { 40 | let f = "ERR Operation against a key holding the wrong kind of value"; 41 | Ok(Frame::Error(f.to_string())) 42 | } 43 | } 44 | }, 45 | None => Ok(Frame::Integer(0)), 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/hash/hvals.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Hvals { 5 | key: String, 6 | } 7 | 8 | impl Hvals { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let key = frame.get_arg(1); 13 | 14 | if key.is_none() { 15 | return Err(Error::msg("ERR wrong number of arguments for 'hvals' command")); 16 | } 17 | 18 | let final_key = key.unwrap().to_string(); // 键 19 | 20 | Ok(Hvals { 21 | key: final_key, 22 | }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::Hash(hash) => { 30 | let mut vals = Vec::new(); 31 | for val in hash.values() { 32 | vals.push(Frame::BulkString(val.clone())); 33 | } 34 | Ok(Frame::Array(vals)) 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | Ok(Frame::Error(f.to_string())) 39 | } 40 | } 41 | }, 42 | None => Ok(Frame::Array(vec![])), 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/cmd/hash/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hmset; 2 | pub mod hmget; 3 | pub mod hstrlen; 4 | pub mod hexists; 5 | pub mod hgetall; 6 | pub mod hsetnx; 7 | pub mod hkeys; 8 | pub mod hvals; 9 | pub mod hset; 10 | pub mod hget; 11 | pub mod hdel; 12 | pub mod hlen; -------------------------------------------------------------------------------- /src/cmd/key/del.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Del { 6 | keys: Vec, 7 | } 8 | 9 | impl Del { 10 | 11 | /** 12 | * 获取键的集合 13 | * 14 | * @param frame 命令帧 15 | */ 16 | pub fn parse_from_frame(frame: Frame) -> Result { 17 | let keys = frame.get_args_from_index(1); 18 | if keys.is_empty() { 19 | return Err(Error::msg("ERR wrong number of arguments for 'del' command")); 20 | } 21 | Ok(Del { 22 | keys: keys 23 | }) 24 | } 25 | 26 | /** 27 | * 执行命令逻辑 28 | * 29 | * @param db 数据库 30 | */ 31 | pub fn apply(self, db: &mut Db) -> Result { 32 | let mut counter: usize = 0; // 使用 usize 作为计数器 33 | for key in self.keys { 34 | match db.remove(&key) { 35 | Some(_) => counter += 1, // 如果键存在,增加计数器 36 | None => { 37 | // 键不存在,不增加计数器 38 | }, 39 | } 40 | } 41 | // 将 usize 转换为 i64 类型 42 | Ok(Frame::Integer(counter as i64)) 43 | } 44 | } -------------------------------------------------------------------------------- /src/cmd/key/exists.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::Db, frame::Frame}; 3 | 4 | pub struct Exists { 5 | key: String, 6 | } 7 | 8 | impl Exists { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let key = frame.get_arg(1); 12 | if key.is_none() { 13 | return Err(Error::msg("ERR wrong number of arguments for 'exists' command")); 14 | } 15 | let key_str = key.unwrap().to_string(); // 键 16 | Ok(Exists { 17 | key: key_str, 18 | }) 19 | } 20 | 21 | pub fn apply(self, db: &Db) -> Result { 22 | if db.exists(&self.key) { 23 | Ok(Frame::Integer(1)) 24 | } else { 25 | Ok(Frame::Integer(0)) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/cmd/key/expire.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Expire { 6 | key: String, 7 | ttl: u64 8 | } 9 | 10 | impl Expire { 11 | 12 | pub fn parse_from_frame(frame: Frame) -> Result { 13 | 14 | let args = frame.get_args(); 15 | 16 | if args.len() < 3 { 17 | return Err(Error::msg("ERR wrong number of arguments for 'expire' command")); 18 | } 19 | 20 | let key = args[1].to_string(); 21 | 22 | let ttl = match args[2].parse::() { 23 | Ok(val) => val * 1000, // 秒 -> 毫秒 24 | Err(_) => { 25 | return Err(Error::msg("ERR value is not an integer or out of range")); 26 | } 27 | }; 28 | 29 | Ok(Expire { 30 | key: key, 31 | ttl: ttl 32 | }) 33 | } 34 | 35 | pub fn apply(self, db: &mut Db) -> Result { 36 | db.expire(self.key.clone(), self.ttl); 37 | Ok(Frame::Ok) 38 | } 39 | } -------------------------------------------------------------------------------- /src/cmd/key/expireat.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | use crate::{db::Db, frame::Frame}; 5 | 6 | pub struct ExpireAt { 7 | key: String, 8 | timestamp: u64, 9 | } 10 | 11 | impl ExpireAt { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | let args = frame.get_args(); 15 | 16 | if args.len() < 3 { 17 | return Err(Error::msg("ERR wrong number of arguments for 'expireat' command")); 18 | } 19 | 20 | let key = args[1].to_string(); 21 | let timestamp = match args[2].parse::() { 22 | Ok(val) => val, 23 | Err(_) => { 24 | return Err(Error::msg("ERR value is not an integer or out of range")); 25 | } 26 | }; 27 | Ok(ExpireAt { key, timestamp }) 28 | } 29 | 30 | pub fn apply(self, db: &mut Db) -> Result { 31 | let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs(); 32 | let ttl = if self.timestamp > now { 33 | ( self.timestamp - now ) * 1000 34 | } else { 35 | 0 36 | }; 37 | db.expire(self.key.clone(), ttl); 38 | Ok(Frame::Ok) 39 | } 40 | } -------------------------------------------------------------------------------- /src/cmd/key/keys.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::Db, frame::Frame}; 3 | 4 | pub struct Keys { 5 | pattern: String, 6 | } 7 | 8 | impl Keys { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args_from_index(1); 12 | if args.len() != 1 { 13 | return Err(Error::msg("KEYS command requires exactly one argument")); 14 | } 15 | Ok(Keys { pattern: args[0].clone() }) 16 | } 17 | 18 | pub fn apply(self, db: &mut Db) -> Result { 19 | let keys = db.keys(&self.pattern); 20 | let results: Vec = keys.into_iter().map(|key| Frame::BulkString(key)).collect(); 21 | Ok(Frame::Array(results)) 22 | } 23 | } -------------------------------------------------------------------------------- /src/cmd/key/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod del; 2 | pub mod expire; 3 | pub mod rename; 4 | pub mod exists; 5 | pub mod persist; 6 | pub mod ttl; 7 | pub mod pttl; 8 | pub mod expireat; 9 | pub mod randomkey; 10 | pub mod renamenx; 11 | pub mod r#type; 12 | pub mod keys; 13 | pub mod pexpire; 14 | pub mod pexpireat; -------------------------------------------------------------------------------- /src/cmd/key/persist.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::Db, frame::Frame}; 3 | 4 | pub struct Persist { 5 | key: String, 6 | } 7 | 8 | impl Persist { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let key = frame.get_arg(1); 12 | if key.is_none() { 13 | return Err(Error::msg("ERR wrong number of arguments for 'persist' command")); 14 | } 15 | let key_str = key.unwrap().to_string(); // 键 16 | Ok(Persist { 17 | key: key_str, 18 | }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | if db.expire_records.contains_key(&self.key) { 23 | match db.expire_records.remove(&self.key) { 24 | Some(_) => Ok(Frame::Integer(1)), 25 | None => { 26 | Ok(Frame::Integer(0)) 27 | } 28 | } 29 | } else { 30 | Ok(Frame::Integer(0)) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/cmd/key/pexpire.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Pexpire { 6 | key: String, 7 | ttl: u64 8 | } 9 | 10 | impl Pexpire { 11 | 12 | pub fn parse_from_frame(frame: Frame) -> Result { 13 | 14 | let args = frame.get_args(); 15 | 16 | if args.len() < 3 { 17 | return Err(Error::msg("ERR wrong number of arguments for 'pexpire' command")); 18 | } 19 | 20 | let key = args[1].to_string(); 21 | 22 | let ttl = match args[2].parse::() { 23 | Ok(val) => val, // 毫秒 24 | Err(_) => { 25 | return Err(Error::msg("ERR value is not an integer or out of range")); 26 | } 27 | }; 28 | 29 | Ok(Pexpire { 30 | key: key, 31 | ttl: ttl 32 | }) 33 | } 34 | 35 | pub fn apply(self, db: &mut Db) -> Result { 36 | db.expire(self.key.clone(), self.ttl); 37 | Ok(Frame::Ok) 38 | } 39 | } -------------------------------------------------------------------------------- /src/cmd/key/pexpireat.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | use crate::{db::Db, frame::Frame}; 5 | 6 | pub struct PexpireAt { 7 | key: String, 8 | timestamp: u64, 9 | } 10 | 11 | impl PexpireAt { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | let args = frame.get_args(); 15 | 16 | if args.len() < 3 { 17 | return Err(Error::msg("ERR wrong number of arguments for 'pexpireat' command")); 18 | } 19 | 20 | let key = args[1].to_string(); 21 | let timestamp = match args[2].parse::() { 22 | Ok(val) => val, 23 | Err(_) => { 24 | return Err(Error::msg("ERR value is not an integer or out of range")); 25 | } 26 | }; 27 | Ok(PexpireAt { key, timestamp }) 28 | } 29 | 30 | pub fn apply(self, db: &mut Db) -> Result { 31 | let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_millis() as u64; 32 | let ttl = if self.timestamp > now { 33 | self.timestamp - now 34 | } else { 35 | 0 36 | }; 37 | db.expire(self.key.clone(), ttl); 38 | Ok(Frame::Ok) 39 | } 40 | } -------------------------------------------------------------------------------- /src/cmd/key/pttl.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Pttl { 6 | key: String, 7 | } 8 | 9 | impl Pttl { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let key = frame.get_arg(1); 13 | if key.is_none() { 14 | return Err(Error::msg("ERR wrong number of arguments for 'pttl' command")); 15 | } 16 | let fianl_key = key.unwrap().to_string(); 17 | Ok(Pttl { 18 | key: fianl_key 19 | }) 20 | } 21 | 22 | pub fn apply(self, db: &mut Db) -> Result { 23 | let millis = db.ttl_millis(&self.key); 24 | Ok(Frame::Integer(millis)) 25 | } 26 | } -------------------------------------------------------------------------------- /src/cmd/key/randomkey.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct RandomKey { 6 | 7 | } 8 | 9 | impl RandomKey { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | if frame.get_args().len() != 1 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'randomkey' command")); 14 | } 15 | Ok(RandomKey {}) 16 | } 17 | 18 | pub fn apply(self, db: &Db) -> Result { 19 | if let Some(key) = db.random_key() { 20 | Ok(Frame::BulkString(key)) 21 | } else { 22 | Ok(Frame::Null) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/cmd/key/rename.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::Db, frame::Frame}; 3 | 4 | pub struct Rename { 5 | old_key: String, 6 | new_key: String, 7 | } 8 | 9 | impl Rename { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let old_key = frame.get_arg(1); 14 | let new_key = frame.get_arg(2); 15 | 16 | if old_key.is_none() || new_key.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'rename' command")); 18 | } 19 | 20 | let old_key_str = old_key.unwrap().to_string(); // 旧键 21 | let new_key_str = new_key.unwrap().to_string(); // 新键 22 | 23 | Ok(Rename { 24 | old_key: old_key_str, 25 | new_key: new_key_str, 26 | }) 27 | } 28 | 29 | pub fn apply(self, db: &mut Db) -> Result { 30 | 31 | if !db.exists(&self.old_key) { 32 | return Err(Error::msg("ERR no such key")); 33 | } 34 | 35 | if let Some(value) = db.remove(&self.old_key) { 36 | db.insert(self.new_key.clone(), value); 37 | } 38 | 39 | Ok(Frame::Ok) 40 | } 41 | } -------------------------------------------------------------------------------- /src/cmd/key/renamenx.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::Db, frame::Frame}; 3 | pub struct Renamenx { 4 | old_key: String, 5 | new_key: String, 6 | } 7 | 8 | impl Renamenx { 9 | pub fn parse_from_frame(frame: Frame) -> Result { 10 | let old_key = frame.get_arg(1); 11 | let new_key = frame.get_arg(2); 12 | 13 | if old_key.is_none() || new_key.is_none() { 14 | return Err(Error::msg("ERR wrong number of arguments for 'renamenx' command")); 15 | } 16 | 17 | let old_key_str = old_key.unwrap().to_string(); 18 | let new_key_str = new_key.unwrap().to_string(); 19 | 20 | Ok(Renamenx { 21 | old_key: old_key_str, 22 | new_key: new_key_str, 23 | }) 24 | } 25 | 26 | pub fn apply(self, db: &mut Db) -> Result { 27 | 28 | if !db.exists(&self.old_key) { 29 | return Err(Error::msg("ERR no such key")); 30 | } 31 | 32 | if db.exists(&self.new_key) { 33 | return Ok(Frame::Integer(0)); 34 | } 35 | 36 | if let Some(value) = db.remove(&self.old_key) { 37 | db.insert(self.new_key.clone(), value); 38 | } 39 | 40 | Ok(Frame::Integer(1)) 41 | } 42 | } -------------------------------------------------------------------------------- /src/cmd/key/ttl.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::Db, frame::Frame}; 4 | 5 | pub struct Ttl { 6 | key: String, 7 | } 8 | 9 | impl Ttl { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let key = frame.get_arg(1); 13 | if key.is_none() { 14 | return Err(Error::msg("ERR wrong number of arguments for 'ttl' command")); 15 | } 16 | let fianl_key = key.unwrap().to_string(); 17 | Ok(Ttl { 18 | key: fianl_key 19 | }) 20 | } 21 | 22 | pub fn apply(self, db: &mut Db) -> Result { 23 | let millis = db.ttl_millis(&self.key); 24 | let second = millis / 1000; 25 | Ok(Frame::Integer(second)) 26 | } 27 | } -------------------------------------------------------------------------------- /src/cmd/key/type.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Type { 6 | key: String, 7 | } 8 | 9 | impl Type { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let key = frame.get_arg(1); 13 | if key.is_none() { 14 | return Err(Error::msg("ERR wrong number of arguments for 'type' command")); 15 | } 16 | let final_key = key.unwrap().to_string(); 17 | Ok(Type { 18 | key: final_key 19 | }) 20 | } 21 | 22 | pub fn apply(self, db: &mut Db) -> Result { 23 | match db.get(&self.key) { 24 | Some(structure) => { 25 | match structure { 26 | Structure::Set(_) => { 27 | Ok(Frame::SimpleString("set".to_string())) 28 | }, 29 | Structure::String(_) => { 30 | Ok(Frame::SimpleString("string".to_string())) 31 | }, 32 | Structure::SortedSet(_) => { 33 | Ok(Frame::SimpleString("zset".to_string())) 34 | }, 35 | Structure::Hash(_) => { 36 | Ok(Frame::SimpleString("hash".to_string())) 37 | }, 38 | Structure::List(_) => { 39 | Ok(Frame::SimpleString("list".to_string())) 40 | } 41 | } 42 | }, 43 | None => { 44 | Ok(Frame::SimpleString("none".to_string())) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/list/lindex.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Lindex { 5 | key: String, 6 | index: i64, 7 | } 8 | 9 | impl Lindex { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let key = frame.get_arg(1); 14 | let index = frame.get_arg(2); 15 | 16 | if key.is_none() || index.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'lindex' command")); 18 | } 19 | 20 | let final_key = key.unwrap().to_string(); // 键 21 | let final_index = index.unwrap().parse::().map_err(|_| Error::msg("ERR value is not an integer or out of range"))?; 22 | 23 | Ok(Lindex { 24 | key: final_key, 25 | index: final_index, 26 | }) 27 | } 28 | 29 | pub fn apply(self, db: &mut Db) -> Result { 30 | match db.get(&self.key) { 31 | Some(structure) => { 32 | match structure { 33 | Structure::List(list) => { 34 | if self.index < 0 { 35 | let index = list.len() as i64 + self.index; 36 | if index < 0 || index as usize >= list.len() { 37 | Ok(Frame::Null) 38 | } else { 39 | Ok(Frame::BulkString(list[index as usize].clone())) 40 | } 41 | } else if self.index as usize >= list.len() { 42 | Ok(Frame::Null) 43 | } else { 44 | Ok(Frame::BulkString(list[self.index as usize].clone())) 45 | } 46 | }, 47 | _ => { 48 | let f = "ERR Operation against a key holding the wrong kind of value"; 49 | Ok(Frame::Error(f.to_string())) 50 | } 51 | } 52 | }, 53 | None => Ok(Frame::Null), 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/cmd/list/llen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Llen { 5 | key: String, 6 | } 7 | 8 | impl Llen { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let key = frame.get_arg(1); 13 | 14 | if key.is_none() { 15 | return Err(Error::msg("ERR wrong number of arguments for 'llen' command")); 16 | } 17 | 18 | let final_key = key.unwrap().to_string(); // 键 19 | 20 | Ok(Llen { 21 | key: final_key, 22 | }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::List(list) => { 30 | Ok(Frame::Integer(list.len() as i64)) 31 | }, 32 | _ => { 33 | let f = "ERR Operation against a key holding the wrong kind of value"; 34 | Ok(Frame::Error(f.to_string())) 35 | } 36 | } 37 | }, 38 | None => Ok(Frame::Integer(0)), 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/cmd/list/lpop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Lpop { 5 | key: String, 6 | } 7 | 8 | impl Lpop { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let args = frame.get_args(); 13 | 14 | if args.len() != 2 { 15 | return Err(Error::msg("ERR wrong number of arguments for 'lpop' command")); 16 | } 17 | 18 | let key = args[1].to_string(); // 键 19 | 20 | Ok(Lpop { key }) 21 | } 22 | 23 | pub fn apply(self, db: &mut Db) -> Result { 24 | match db.get_mut(&self.key) { 25 | Some(structure) => { 26 | match structure { 27 | Structure::List(list) => { 28 | if list.is_empty() { 29 | Ok(Frame::Null) 30 | } else { 31 | let value = list.remove(0); // 移除列表的第一个元素 32 | Ok(Frame::BulkString(value)) 33 | } 34 | }, 35 | _ => { 36 | let f = "ERR Operation against a key holding the wrong kind of value"; 37 | Ok(Frame::Error(f.to_string())) 38 | } 39 | } 40 | }, 41 | None => { 42 | Ok(Frame::Null) 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/cmd/list/lpush.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Lpush { 5 | key: String, 6 | values: Vec, 7 | } 8 | 9 | impl Lpush { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let args = frame.get_args(); 14 | 15 | if args.len() < 3 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'lpush' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | let values: Vec = args.iter().skip(2).map(|v| v.to_string()).collect(); // 值 21 | 22 | Ok(Lpush { key, values }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get_mut(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::List(list) => { 30 | for value in self.values.into_iter().rev() { 31 | list.insert(0, value); // 向引用 mut 中添加数据 32 | } 33 | Ok(Frame::Integer(list.len() as i64)) 34 | }, 35 | _ => { 36 | let f = "ERR Operation against a key holding the wrong kind of value"; 37 | Ok(Frame::Error(f.to_string())) 38 | } 39 | } 40 | }, 41 | None => { 42 | let mut list = Vec::new(); 43 | for value in self.values.into_iter().rev() { 44 | list.insert(0, value); // 倒序遍历 45 | } 46 | db.insert(self.key.clone(), Structure::List(list.clone())); 47 | Ok(Frame::Integer(list.len() as i64)) 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/cmd/list/lpushx.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Lpushx { 5 | key: String, 6 | values: Vec, 7 | } 8 | 9 | impl Lpushx { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let args = frame.get_args(); 14 | 15 | if args.len() < 3 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'lpushx' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | let values: Vec = args.iter().skip(2).map(|v| v.to_string()).collect(); // 值 21 | 22 | Ok(Lpushx { key, values }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.get_mut(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::List(list) => { 30 | for value in self.values.into_iter().rev() { 31 | list.insert(0, value); // 向引用 mut 中添加数据 32 | } 33 | Ok(Frame::Integer(list.len() as i64)) 34 | }, 35 | _ => { 36 | let f = "ERR Operation against a key holding the wrong kind of value"; 37 | Ok(Frame::Error(f.to_string())) 38 | } 39 | } 40 | }, 41 | None => { 42 | Ok(Frame::Integer(0)) 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/cmd/list/lrange.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Lrange { 5 | key: String, 6 | start: i64, 7 | stop: i64, 8 | } 9 | 10 | impl Lrange { 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let key = frame.get_arg(1); 13 | let start = frame.get_arg(2); 14 | let stop = frame.get_arg(3); 15 | 16 | if key.is_none() || start.is_none() || stop.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'lrange' command")); 18 | } 19 | 20 | let final_key = key.unwrap().to_string(); // 键 21 | 22 | let start = match start.unwrap().parse::() { 23 | Ok(n) => n, 24 | Err(_) => return Err(Error::msg("ERR value is not an integer or out of range")), 25 | }; 26 | 27 | let stop = match stop.unwrap().parse::() { 28 | Ok(n) => n, 29 | Err(_) => return Err(Error::msg("ERR value is not an integer or out of range")), 30 | }; 31 | 32 | Ok(Lrange { 33 | key: final_key, 34 | start, 35 | stop, 36 | }) 37 | } 38 | 39 | pub fn apply(self, db: &mut Db) -> Result { 40 | match db.get(&self.key) { 41 | Some(structure) => { 42 | match structure { 43 | Structure::List(list) => { 44 | 45 | let len = list.len() as i64; 46 | let start = if self.start < 0 { len + self.start } else { self.start }; 47 | let stop = if self.stop < 0 { len + self.stop } else { self.stop }; 48 | 49 | let start = std::cmp::max(0, start); 50 | let stop = std::cmp::min(len - 1, stop); 51 | 52 | if start > stop || start >= len || stop < 0 { 53 | return Ok(Frame::Array(vec![])); 54 | } 55 | 56 | let result: Vec = list.iter() 57 | .skip(start as usize) 58 | .take((stop - start + 1) as usize) 59 | .map(|item| Frame::BulkString(item.clone())) 60 | .collect(); 61 | 62 | Ok(Frame::Array(result)) 63 | }, 64 | _ => { 65 | let f = "ERR Operation against a key holding the wrong kind of value"; 66 | Ok(Frame::Error(f.to_string())) 67 | } 68 | } 69 | }, 70 | None => Ok(Frame::Array(vec![])), 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/cmd/list/lset.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Lset { 6 | key: String, 7 | index: isize, // 索引,支持负数索引 8 | value: String, // 要设置的值 9 | } 10 | 11 | impl Lset { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | 15 | let args = frame.get_args(); 16 | 17 | if args.len() != 4 { 18 | return Err(Error::msg("ERR wrong number of arguments for 'lset' command")); 19 | } 20 | 21 | let key = args[1].to_string(); // 键 22 | let index = args[2].parse::().map_err(|_| Error::msg("ERR value is not an integer or out of range"))?; // 索引 23 | let value = args[3].to_string(); // 要设置的值 24 | 25 | Ok(Lset { key, index, value }) 26 | } 27 | 28 | pub fn apply(self, db: &mut Db) -> Result { 29 | match db.get_mut(&self.key) { 30 | Some(structure) => { 31 | match structure { 32 | Structure::List(list) => { 33 | if list.is_empty() { 34 | Ok(Frame::Error("ERR index out of range".to_string())) 35 | } else { 36 | let list_len = list.len() as isize; 37 | let adjusted_index = if self.index < 0 { 38 | list_len + self.index 39 | } else { 40 | self.index 41 | }; 42 | 43 | if adjusted_index < 0 || adjusted_index >= list_len { 44 | Ok(Frame::Error("ERR index out of range".to_string())) 45 | } else { 46 | list[adjusted_index as usize] = self.value; 47 | Ok(Frame::SimpleString("OK".to_string())) 48 | } 49 | } 50 | }, 51 | _ => { 52 | let f = "ERR Operation against a key holding the wrong kind of value"; 53 | Ok(Frame::Error(f.to_string())) 54 | } 55 | } 56 | }, 57 | None => { 58 | let f = "ERR no such key"; 59 | Ok(Frame::Error(f.to_string())) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/cmd/list/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lpush; 2 | pub mod rpush; 3 | pub mod rpushx; 4 | pub mod lindex; 5 | pub mod lpushx; 6 | pub mod lrange; 7 | pub mod lpop; 8 | pub mod rpop; 9 | pub mod llen; 10 | pub mod lset; -------------------------------------------------------------------------------- /src/cmd/list/rpop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Rpop { 6 | key: String, 7 | } 8 | 9 | impl Rpop { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let args = frame.get_args(); 14 | 15 | if args.len() != 2 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'rpop' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | 21 | Ok(Rpop { key }) 22 | } 23 | 24 | pub fn apply(self, db: &mut Db) -> Result { 25 | match db.get_mut(&self.key) { 26 | Some(structure) => { 27 | match structure { 28 | Structure::List(list) => { 29 | if list.is_empty() { 30 | Ok(Frame::Null) 31 | } else { 32 | let value = list.pop(); // 移除列表的最后一个元素 33 | match value { 34 | Some(val) => Ok(Frame::BulkString(val)), 35 | None => Ok(Frame::Null), // 理论上不会执行到这,因为前面已经判断过列表不为空 36 | } 37 | } 38 | }, 39 | _ => { 40 | let f = "ERR Operation against a key holding the wrong kind of value"; 41 | Ok(Frame::Error(f.to_string())) 42 | } 43 | } 44 | }, 45 | None => { 46 | Ok(Frame::Null) 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/cmd/list/rpush.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Rpush { 5 | key: String, 6 | values: Vec, 7 | } 8 | 9 | impl Rpush { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() < 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'rpush' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let values: Vec = args.iter().skip(2).map(|v| v.to_string()).collect(); // 值 18 | Ok(Rpush { key, values }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | match db.get_mut(&self.key) { 23 | Some(structure) => { 24 | match structure { 25 | Structure::List(list) => { 26 | for value in self.values { 27 | list.push(value); // 向引用 mut 中添加数据 28 | } 29 | Ok(Frame::Integer(list.len() as i64)) 30 | }, 31 | _ => { 32 | let f = "ERR Operation against a key holding the wrong kind of value"; 33 | Ok(Frame::Error(f.to_string())) 34 | } 35 | } 36 | }, 37 | None => { 38 | let mut list = Vec::new(); 39 | for value in self.values { 40 | list.push(value); // 正序遍历 41 | } 42 | db.insert(self.key.clone(), Structure::List(list.clone())); 43 | Ok(Frame::Integer(list.len() as i64)) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/cmd/list/rpushx.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Rpushx { 5 | key: String, 6 | values: Vec, 7 | } 8 | 9 | impl Rpushx { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() < 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'rpushx' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let values: Vec = args.iter().skip(2).map(|v| v.to_string()).collect(); // 值 18 | Ok(Rpushx { key, values }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | match db.get_mut(&self.key) { 23 | Some(structure) => { 24 | match structure { 25 | Structure::List(list) => { 26 | for value in self.values { 27 | list.push(value); // 向引用 mut 中添加数据 28 | } 29 | Ok(Frame::Integer(list.len() as i64)) 30 | }, 31 | _ => { 32 | let f = "ERR Operation against a key holding the wrong kind of value"; 33 | Ok(Frame::Error(f.to_string())) 34 | } 35 | } 36 | }, 37 | None => { 38 | Ok(Frame::Integer(0)) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod key; 2 | pub mod select; 3 | pub mod string; 4 | pub mod auth; 5 | pub mod flushall; 6 | pub mod flushdb; 7 | pub mod ping; 8 | pub mod dbsize; 9 | pub mod hash; 10 | pub mod list; 11 | pub mod unknown; 12 | pub mod sorted_set; 13 | pub mod set; 14 | pub mod echo; 15 | -------------------------------------------------------------------------------- /src/cmd/ping.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::frame::Frame; 4 | 5 | pub struct Ping; 6 | 7 | impl Ping { 8 | 9 | pub fn parse_from_frame(_frame: Frame) -> Result { 10 | Ok(Ping) 11 | } 12 | 13 | pub fn apply(self) -> Result { 14 | Ok(Frame::SimpleString("PONG".to_string())) 15 | } 16 | } -------------------------------------------------------------------------------- /src/cmd/select.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{frame::Frame, server_handler::ServerHandler}; 4 | 5 | pub struct Select { 6 | db: usize, 7 | } 8 | 9 | impl Select { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let db_string = frame.get_arg(1); 14 | let db_string = match db_string { 15 | Some(s) => s, 16 | None => { 17 | return Err(Error::msg("ERR wrong number of arguments for 'select' command")) 18 | } 19 | }; 20 | 21 | let db: usize = match db_string.parse::() { 22 | Ok(num) => num, 23 | Err(_) => { 24 | return Err(Error::msg("ERR invalid DB index")); 25 | } 26 | }; 27 | 28 | Ok(Select { db: db }) 29 | } 30 | 31 | pub fn apply(self, handler:&mut ServerHandler) -> Result { 32 | match handler.change_sender(self.db) { 33 | Ok(_) => Ok(Frame::Ok), 34 | Err(e) => { 35 | Ok(Frame::Error(e.to_string())) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/cmd/set/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sadd; 2 | pub mod smembers; 3 | pub mod sismember; 4 | pub mod scard; 5 | pub mod spop; 6 | pub mod srem; 7 | pub mod sunion; 8 | pub mod sinter; 9 | pub mod sunionstore; -------------------------------------------------------------------------------- /src/cmd/set/sadd.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Sadd { 7 | key: String, 8 | members: Vec, 9 | } 10 | 11 | impl Sadd { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | 15 | let args = frame.get_args(); 16 | 17 | if args.len() < 3 { 18 | return Err(Error::msg("ERR wrong number of arguments for 'sadd' command")); 19 | } 20 | 21 | let key = args[1].to_string(); // 键 22 | let members: Vec = args.iter().skip(2).map(|v| v.to_string()).collect(); // 成员 23 | 24 | Ok(Sadd { key, members }) 25 | } 26 | 27 | pub fn apply(self, db: &mut Db) -> Result { 28 | match db.records.get_mut(&self.key) { 29 | Some(structure) => { 30 | match structure { 31 | Structure::Set(set) => { 32 | let mut added_count = 0; 33 | for member in self.members { 34 | if set.insert(member) { 35 | added_count += 1; 36 | } 37 | } 38 | Ok(Frame::Integer(added_count as i64)) 39 | }, 40 | _ => { 41 | let f = "ERR Operation against a key holding the wrong kind of value"; 42 | Ok(Frame::Error(f.to_string())) 43 | } 44 | } 45 | }, 46 | None => { 47 | let mut set = HashSet::new(); 48 | let mut added_count = 0; 49 | for member in self.members { 50 | if set.insert(member) { 51 | added_count += 1; 52 | } 53 | } 54 | db.records.insert(self.key.clone(), Structure::Set(set)); 55 | Ok(Frame::Integer(added_count as i64)) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/cmd/set/scard.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Scard { 5 | key: String, 6 | } 7 | 8 | impl Scard { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let args = frame.get_args(); 13 | 14 | if args.len() != 2 { 15 | return Err(Error::msg("ERR wrong number of arguments for 'scard' command")); 16 | } 17 | 18 | let key = args[1].to_string(); // 键 19 | 20 | Ok(Scard { key }) 21 | } 22 | 23 | pub fn apply(self, db: &mut Db) -> Result { 24 | match db.records.get(&self.key) { 25 | Some(structure) => { 26 | match structure { 27 | Structure::Set(set) => { 28 | Ok(Frame::Integer(set.len() as i64)) 29 | }, 30 | _ => { 31 | let f = "ERR Operation against a key holding the wrong kind of value"; 32 | Ok(Frame::Error(f.to_string())) 33 | } 34 | } 35 | }, 36 | None => { 37 | Ok(Frame::Integer(0)) 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/cmd/set/sinter.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | use std::collections::HashSet; 4 | 5 | pub struct Sinter { 6 | keys: Vec, 7 | } 8 | 9 | impl Sinter { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() < 2 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'sinter' command")); 15 | } 16 | let keys: Vec = args[1..].iter().map(|arg| arg.to_string()).collect(); 17 | Ok(Sinter { keys }) 18 | } 19 | 20 | pub fn apply(self, db: &mut Db) -> Result { 21 | 22 | let mut iter = self.keys.iter(); 23 | let first = iter.next().unwrap(); 24 | 25 | match db.records.get(first) { 26 | Some(structure) => { 27 | match structure { 28 | Structure::Set(first_set) => { 29 | let mut intersection: HashSet = first_set.clone(); 30 | for key in iter { 31 | match db.records.get(key) { 32 | Some(structure) => { 33 | match structure { 34 | Structure::Set(set) => { 35 | intersection = intersection.intersection(set).cloned().collect(); 36 | }, 37 | _ => { 38 | let f = "ERR Operation against a key holding the wrong kind of value"; 39 | return Ok(Frame::Error(f.to_string())); 40 | } 41 | } 42 | }, 43 | None => { 44 | intersection.clear(); 45 | break; 46 | } 47 | } 48 | } 49 | let mut result = Vec::new(); 50 | for member in intersection.iter() { 51 | result.push(Frame::BulkString(member.clone())); 52 | } 53 | Ok(Frame::Array(result)) 54 | }, 55 | _ => { 56 | let f = "ERR Operation against a key holding the wrong kind of value"; 57 | Ok(Frame::Error(f.to_string())) 58 | } 59 | } 60 | }, 61 | None => { 62 | Ok(Frame::Array(Vec::new())) 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/cmd/set/sismember.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Sismember { 5 | key: String, 6 | member: String, 7 | } 8 | 9 | impl Sismember { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() != 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'sismember' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let member = args[2].to_string(); // 成员 18 | Ok(Sismember { key, member }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | match db.records.get(&self.key) { 23 | Some(structure) => { 24 | match structure { 25 | Structure::Set(set) => { 26 | // 判断成员是否存在于集合中 27 | let is_member = set.contains(&self.member); 28 | Ok(Frame::Integer(is_member as i64)) // 如果存在返回 1,否则返回 0 29 | }, 30 | _ => { 31 | let f = "ERR Operation against a key holding the wrong kind of value"; 32 | Ok(Frame::Error(f.to_string())) 33 | } 34 | } 35 | }, 36 | None => { 37 | // 如果键不存在,返回 0 38 | Ok(Frame::Integer(0)) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/cmd/set/smembers.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Smembers { 5 | key: String, 6 | } 7 | 8 | impl Smembers { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | 12 | let args = frame.get_args(); 13 | 14 | if args.len() != 2 { 15 | return Err(Error::msg("ERR wrong number of arguments for 'smembers' command")); 16 | } 17 | 18 | let key = args[1].to_string(); // 键 19 | 20 | Ok(Smembers { key }) 21 | } 22 | 23 | pub fn apply(self, db: &mut Db) -> Result { 24 | match db.records.get(&self.key) { 25 | Some(structure) => { 26 | match structure { 27 | Structure::Set(set) => { 28 | let mut members = Vec::new(); 29 | for member in set.iter() { 30 | members.push(Frame::BulkString(member.clone())); 31 | } 32 | Ok(Frame::Array(members)) 33 | }, 34 | _ => { 35 | let f = "ERR Operation against a key holding the wrong kind of value"; 36 | Ok(Frame::Error(f.to_string())) 37 | } 38 | } 39 | }, 40 | None => { 41 | Ok(Frame::Array(Vec::new())) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/cmd/set/spop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Spop { 5 | key: String, 6 | count: Option, 7 | } 8 | 9 | impl Spop { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let args = frame.get_args(); 14 | 15 | if args.len() < 2 || args.len() > 3 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'spop' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | let count = if args.len() == 3 { 21 | match args[2].parse::() { 22 | Ok(c) => Some(c), 23 | Err(_) => return Err(Error::msg("ERR value is not an integer or out of range")), 24 | } 25 | } else { 26 | None 27 | }; 28 | 29 | Ok(Spop { key, count }) 30 | } 31 | 32 | pub fn apply(self, db: &mut Db) -> Result { 33 | match db.records.get_mut(&self.key) { 34 | Some(structure) => { 35 | match structure { 36 | Structure::Set(set) => { 37 | if set.is_empty() { 38 | Ok(Frame::Null) 39 | } else { 40 | let pop_count = self.count.unwrap_or(1); 41 | let mut popped_members = Vec::new(); 42 | for _ in 0..pop_count { 43 | if let Some(member) = set.iter().next().cloned() { 44 | set.remove(&member); 45 | popped_members.push(Frame::BulkString(member)); 46 | } else { 47 | break; 48 | } 49 | } 50 | if pop_count == 1 { 51 | Ok(popped_members.pop().unwrap_or(Frame::Null)) 52 | } else { 53 | Ok(Frame::Array(popped_members)) 54 | } 55 | } 56 | }, 57 | _ => { 58 | let f = "ERR Operation against a key holding the wrong kind of value"; 59 | Ok(Frame::Error(f.to_string())) 60 | } 61 | } 62 | }, 63 | None => { 64 | Ok(Frame::Null) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/cmd/set/srem.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Srem { 5 | key: String, 6 | members: Vec, 7 | } 8 | 9 | impl Srem { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let args = frame.get_args(); 14 | 15 | if args.len() < 3 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'srem' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | let members = args[2..].iter().map(|arg| arg.to_string()).collect(); // 要移除的成员 21 | 22 | Ok(Srem { key, members }) 23 | } 24 | 25 | pub fn apply(self, db: &mut Db) -> Result { 26 | match db.records.get_mut(&self.key) { 27 | Some(structure) => { 28 | match structure { 29 | Structure::Set(set) => { 30 | let mut removed_count = 0; 31 | for member in &self.members { 32 | if set.remove(member) { 33 | removed_count += 1; 34 | } 35 | } 36 | Ok(Frame::Integer(removed_count as i64)) 37 | }, 38 | _ => { 39 | let f = "ERR Operation against a key holding the wrong kind of value"; 40 | Ok(Frame::Error(f.to_string())) 41 | } 42 | } 43 | }, 44 | None => { 45 | Ok(Frame::Integer(0)) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/cmd/set/sunion.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Sunion { 7 | keys: Vec, 8 | } 9 | 10 | impl Sunion { 11 | 12 | pub fn parse_from_frame(frame: Frame) -> Result { 13 | let args = frame.get_args(); 14 | 15 | // 至少需要两个键(一个命令名,一个或多个集合键) 16 | if args.len() < 2 { 17 | return Err(Error::msg("ERR wrong number of arguments for 'sunion' command")); 18 | } 19 | 20 | // 提取所有键 21 | let keys = args[1..].iter().map(|arg| arg.to_string()).collect(); 22 | 23 | Ok(Sunion { keys }) 24 | } 25 | 26 | pub fn apply(self, db: &mut Db) -> Result { 27 | let mut result_set = HashSet::new(); 28 | for key in self.keys { 29 | if let Some(structure) = db.records.get(&key) { 30 | match structure { 31 | Structure::Set(set) => { 32 | for member in set.iter() { 33 | result_set.insert(member.clone()); 34 | } 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | return Ok(Frame::Error(f.to_string())); 39 | } 40 | } 41 | } 42 | } 43 | 44 | // 将结果转换为 Frame::Array 45 | let members: Vec = result_set.into_iter() 46 | .map(|member| Frame::BulkString(member)) 47 | .collect(); 48 | 49 | Ok(Frame::Array(members)) 50 | } 51 | } -------------------------------------------------------------------------------- /src/cmd/set/sunionstore.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use anyhow::Error; 4 | use crate::{db::{Db, Structure}, frame::Frame}; 5 | 6 | pub struct Sunionstore { 7 | destination: String, 8 | keys: Vec, 9 | } 10 | 11 | impl Sunionstore { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | let args = frame.get_args(); 15 | 16 | // 至少需要三个参数(命令名、目标集合键、一个或多个源集合键) 17 | if args.len() < 3 { 18 | return Err(Error::msg("ERR wrong number of arguments for 'sunionstore' command")); 19 | } 20 | 21 | let destination = args[1].to_string(); 22 | let keys = args[2..].iter().map(|arg| arg.to_string()).collect(); 23 | 24 | Ok(Sunionstore { destination, keys }) 25 | } 26 | 27 | pub fn apply(self, db: &mut Db) -> Result { 28 | let destination = self.destination; 29 | let keys = self.keys; 30 | 31 | // 创建一个空的 HashSet 用于存储并集结果 32 | let mut result_set = HashSet::new(); 33 | 34 | for key in keys { 35 | if let Some(structure) = db.records.get(&key) { 36 | match structure { 37 | Structure::Set(set) => { 38 | for member in set.iter() { 39 | result_set.insert(member.clone()); 40 | } 41 | } 42 | _ => { 43 | let f = "ERR Operation against a key holding the wrong kind of value"; 44 | return Ok(Frame::Error(f.to_string())); 45 | } 46 | } 47 | } 48 | } 49 | db.records.insert(destination, Structure::Set(result_set.clone())); 50 | Ok(Frame::Integer(result_set.len() as i64)) 51 | } 52 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zcount; 2 | pub mod zadd; 3 | pub mod zrem; 4 | pub mod zscore; 5 | pub mod zcard; 6 | pub mod zrank; -------------------------------------------------------------------------------- /src/cmd/sorted_set/zadd.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use anyhow::Error; 4 | 5 | use crate::{db::{Db, Structure}, frame::Frame}; 6 | 7 | pub struct Zadd { 8 | key: String, 9 | members: Vec<(f64, String)>, // 成员及其分数 10 | } 11 | 12 | impl Zadd { 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | let args = frame.get_args(); 15 | if args.len() < 4 || args.len() % 2 != 0 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'zadd' command")); 17 | } 18 | 19 | let key = args[1].to_string(); // 键 20 | let mut members = Vec::new(); 21 | 22 | for chunk in args[2..].chunks(2) { 23 | if chunk.len() != 2 { 24 | return Err(Error::msg("ERR wrong number of arguments for 'zadd' command")); 25 | } 26 | let score = chunk[0].parse::().map_err(|_| Error::msg("ERR score is not a valid float"))?; 27 | let member = chunk[1].to_string(); 28 | members.push((score, member)); 29 | } 30 | 31 | Ok(Zadd { key, members }) 32 | } 33 | 34 | pub fn apply(self, db: &mut Db) -> Result { 35 | let mut added_count = 0; 36 | 37 | match db.records.get_mut(&self.key) { 38 | Some(structure) => { 39 | match structure { 40 | Structure::SortedSet(set) => { 41 | for (score, member) in self.members { 42 | if set.insert(member, score).is_none() { 43 | added_count += 1; // 成员新增成功 44 | } 45 | } 46 | }, 47 | _ => { 48 | let f = "ERR Operation against a key holding the wrong kind of value"; 49 | return Ok(Frame::Error(f.to_string())); 50 | } 51 | } 52 | }, 53 | None => { // 键不存在,创建一个新的有序集合并插入所有成员 54 | let mut set = BTreeMap::new(); 55 | for (score, member) in self.members { 56 | set.insert(member, score); 57 | added_count += 1; // 成员新增成功 58 | } 59 | db.records.insert(self.key, Structure::SortedSet(set)); 60 | } 61 | } 62 | 63 | Ok(Frame::Integer(added_count as i64)) 64 | } 65 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/zcard.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Zcard { 6 | key: String, 7 | } 8 | 9 | impl Zcard { 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | if args.len() != 2 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'zcard' command")); 14 | } 15 | let key = args[1].to_string(); // 键 16 | Ok(Zcard { key }) 17 | } 18 | 19 | pub fn apply(self, db: &mut Db) -> Result { 20 | match db.records.get(&self.key) { 21 | Some(structure) => { 22 | match structure { 23 | Structure::SortedSet(set) => { 24 | // 返回有序集合的元素数量 25 | Ok(Frame::Integer(set.len() as i64)) 26 | } 27 | _ => { 28 | // 如果键的类型不是有序集合,返回错误 29 | let f = "ERR Operation against a key holding the wrong kind of value"; 30 | Ok(Frame::Error(f.to_string())) 31 | } 32 | } 33 | } 34 | None => { 35 | // 如果键不存在,返回 0 36 | Ok(Frame::Integer(0)) 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/zcount.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Zcount { 6 | key: String, 7 | min: f64, 8 | max: f64, 9 | } 10 | 11 | impl Zcount { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result { 14 | let args = frame.get_args(); 15 | if args.len() != 4 { 16 | return Err(Error::msg("ERR wrong number of arguments for 'zcount' command")); 17 | } 18 | let key = args[1].to_string(); // 键 19 | let min = args[2].parse::().map_err(|_| Error::msg("ERR min is not a valid float"))?; 20 | let max = args[3].parse::().map_err(|_| Error::msg("ERR max is not a valid float"))?; 21 | Ok(Zcount { key, min, max }) 22 | } 23 | 24 | pub fn apply(self, db: &mut Db) -> Result { 25 | match db.records.get(&self.key) { 26 | Some(structure) => { 27 | match structure { 28 | Structure::SortedSet(set) => { 29 | let count = set.values().filter(|&&score| score >= self.min && score <= self.max).count(); 30 | Ok(Frame::Integer(count as i64)) 31 | }, 32 | _ => { 33 | let f = "ERR Operation against a key holding the wrong kind of value"; 34 | Ok(Frame::Error(f.to_string())) 35 | } 36 | } 37 | }, 38 | None => { 39 | Ok(Frame::Integer(0)) // 如果键不存在,返回 0 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/zrank.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Zrank { 6 | key: String, 7 | member: String, 8 | } 9 | 10 | impl Zrank { 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() != 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'zrank' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let member = args[2].to_string(); // 成员 18 | Ok(Zrank { key, member }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | match db.records.get(&self.key) { 23 | Some(structure) => { 24 | match structure { 25 | Structure::SortedSet(set) => { 26 | // 获取成员的分数 27 | if let Some(score) = set.get(&self.member) { 28 | // 计算排名(从小到大) 29 | let rank = set.values().filter(|&&s| s < *score).count(); 30 | Ok(Frame::Integer(rank as i64)) 31 | } else { 32 | // 如果成员不存在,返回 nil 33 | Ok(Frame::Null) 34 | } 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | Ok(Frame::Error(f.to_string())) 39 | } 40 | } 41 | }, 42 | None => { 43 | // 如果键不存在,返回 nil 44 | Ok(Frame::Null) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/zrem.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Zrem { 5 | key: String, 6 | members: Vec, 7 | } 8 | 9 | impl Zrem { 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | if args.len() < 3 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'zrem' command")); 14 | } 15 | let key = args[1].to_string(); // 键 16 | let members = args[2..].iter().map(|arg| arg.to_string()).collect(); // 要移除的成员 17 | Ok(Zrem { key, members }) 18 | } 19 | 20 | pub fn apply(self, db: &mut Db) -> Result { 21 | match db.records.get_mut(&self.key) { 22 | Some(structure) => { 23 | match structure { 24 | Structure::SortedSet(set) => { 25 | let mut removed_count = 0; 26 | for member in &self.members { 27 | if set.remove(member).is_some() { 28 | removed_count += 1; 29 | } 30 | } 31 | Ok(Frame::Integer(removed_count as i64)) 32 | }, 33 | _ => { 34 | let f = "ERR Operation against a key holding the wrong kind of value"; 35 | Ok(Frame::Error(f.to_string())) 36 | } 37 | } 38 | }, 39 | None => { // 如果键不存在,返回 0 40 | Ok(Frame::Integer(0)) 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/cmd/sorted_set/zscore.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Zscore { 6 | key: String, 7 | member: String, 8 | } 9 | 10 | impl Zscore { 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() != 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'zscore' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let member = args[2].to_string(); // 成员 18 | Ok(Zscore { key, member }) 19 | } 20 | 21 | pub fn apply(self, db: &mut Db) -> Result { 22 | match db.records.get(&self.key) { 23 | Some(structure) => { 24 | match structure { 25 | Structure::SortedSet(set) => { 26 | if let Some(score) = set.get(&self.member) { 27 | Ok(Frame::BulkString(score.to_string())) 28 | } else { 29 | Ok(Frame::Null) 30 | } 31 | }, 32 | _ => { 33 | let f = "ERR Operation against a key holding the wrong kind of value"; 34 | Ok(Frame::Error(f.to_string())) 35 | } 36 | } 37 | }, 38 | None => { 39 | Ok(Frame::Null) 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/cmd/string/append.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Append { 5 | key: String, 6 | val: String, 7 | } 8 | 9 | impl Append { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let key = frame.get_arg(1); 14 | let val = frame.get_arg(2); 15 | 16 | if key.is_none() || val.is_none() { 17 | return Err(Error::msg("ERR wrong number of arguments for 'append' command")); 18 | } 19 | 20 | let key_str = key.unwrap().to_string(); // 键 21 | let val_str = val.unwrap().to_string(); // 值 22 | 23 | Ok(Append { 24 | key: key_str, 25 | val: val_str, 26 | }) 27 | } 28 | 29 | pub fn apply(self, db: &mut Db) -> Result { 30 | 31 | let existing_value = match db.get(&self.key) { 32 | Some(Structure::String(s)) => s, 33 | Some(_) => return Err(Error::msg("ERR wrong type for 'append' command")), 34 | None => &String::new(), 35 | }; 36 | 37 | let new_value = format!("{}{}", existing_value, self.val); 38 | db.insert(self.key, Structure::String(new_value)); 39 | 40 | Ok(Frame::Ok) 41 | } 42 | } -------------------------------------------------------------------------------- /src/cmd/string/decr.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Decr { 5 | key: String, 6 | } 7 | 8 | impl Decr { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | if args.len() != 2 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'decr' command")); 14 | } 15 | let key = args[1].to_string(); // 键 16 | Ok(Decr { key }) 17 | } 18 | 19 | pub fn apply(self, db: &mut Db) -> Result { 20 | match db.get_mut(&self.key) { 21 | Some(structure) => { 22 | match structure { 23 | Structure::String(str) => { 24 | match str.parse::() { 25 | Ok(mut num) => { 26 | num -= 1; 27 | *str = num.to_string(); 28 | Ok(Frame::Integer(num)) 29 | }, 30 | Err(_) => { 31 | let f = "ERR value is not an integer or out of range"; 32 | Ok(Frame::Error(f.to_string())) 33 | } 34 | } 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | Ok(Frame::Error(f.to_string())) 39 | } 40 | } 41 | }, 42 | None => { 43 | db.insert(self.key.clone(), Structure::String("-1".to_string())); 44 | Ok(Frame::Integer(-1)) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/string/decrby.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Decrby { 5 | key: String, 6 | decrement: i64, 7 | } 8 | 9 | impl Decrby { 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | if args.len() != 3 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'decrby' command")); 14 | } 15 | let key = args[1].to_string(); // 键 16 | let decrement = args[2].parse::().map_err(|_| { 17 | Error::msg("ERR value is not an integer or out of range") 18 | })?; 19 | Ok(Decrby { key, decrement }) 20 | } 21 | 22 | pub fn apply(self, db: &mut Db) -> Result { 23 | match db.get_mut(&self.key) { 24 | Some(structure) => { 25 | match structure { 26 | Structure::String(str) => { 27 | match str.parse::() { 28 | Ok(mut num) => { 29 | num -= self.decrement; 30 | *str = num.to_string(); 31 | Ok(Frame::Integer(num)) 32 | }, 33 | Err(_) => { 34 | let f = "ERR value is not an integer or out of range"; 35 | Ok(Frame::Error(f.to_string())) 36 | } 37 | } 38 | }, 39 | _ => { 40 | let f = "ERR Operation against a key holding the wrong kind of value"; 41 | Ok(Frame::Error(f.to_string())) 42 | } 43 | } 44 | }, 45 | None => { 46 | let new_value = -self.decrement; 47 | db.insert(self.key.clone(), Structure::String(new_value.to_string())); 48 | Ok(Frame::Integer(new_value)) 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/cmd/string/get.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Get { 6 | key: String, 7 | } 8 | 9 | impl Get { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | 13 | let key = frame.get_arg(1); 14 | 15 | if key.is_none() { 16 | return Err(Error::msg("ERR wrong number of arguments for 'get' command")); 17 | } 18 | 19 | let fianl_key = key.unwrap().to_string(); 20 | 21 | Ok(Get { 22 | key: fianl_key 23 | }) 24 | } 25 | 26 | pub fn apply(self, db: &mut Db) -> Result { 27 | let result_structure = db.get(&self.key); 28 | match result_structure { 29 | Some(structure) => { 30 | match structure { 31 | Structure::String(value) => { 32 | Ok(Frame::SimpleString(value.to_string())) 33 | }, 34 | _ => { 35 | Ok(Frame::Error("Type parsing error".to_string())) 36 | } 37 | } 38 | }, 39 | None => { 40 | Ok(Frame::Null) 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/cmd/string/incr.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Incr { 5 | key: String, 6 | } 7 | 8 | impl Incr { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let args = frame.get_args(); 12 | if args.len() != 2 { 13 | return Err(Error::msg("ERR wrong number of arguments for 'incr' command")); 14 | } 15 | let key = args[1].to_string(); // 键 16 | Ok(Incr { key }) 17 | } 18 | 19 | pub fn apply(self, db: &mut Db) -> Result { 20 | match db.get_mut(&self.key) { 21 | Some(structure) => { 22 | match structure { 23 | Structure::String(str) => { 24 | match str.parse::() { 25 | Ok(mut num) => { 26 | num += 1; 27 | *str = num.to_string(); 28 | Ok(Frame::Integer(num)) 29 | }, 30 | Err(_) => { 31 | let f = "ERR value is not an integer or out of range"; 32 | Ok(Frame::Error(f.to_string())) 33 | } 34 | } 35 | }, 36 | _ => { 37 | let f = "ERR Operation against a key holding the wrong kind of value"; 38 | Ok(Frame::Error(f.to_string())) 39 | } 40 | } 41 | }, 42 | None => { 43 | db.insert(self.key.clone(), Structure::String("1".to_string())); 44 | Ok(Frame::Integer(1)) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/cmd/string/incrby.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Incrby { 5 | key: String, 6 | increment: i64, 7 | } 8 | 9 | impl Incrby { 10 | 11 | pub fn parse_from_frame(frame: Frame) -> Result { 12 | let args = frame.get_args(); 13 | if args.len() != 3 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'incrby' command")); 15 | } 16 | let key = args[1].to_string(); // 键 17 | let increment = args[2].parse::().map_err(|_| { 18 | Error::msg("ERR value is not an integer or out of range") 19 | })?; 20 | Ok(Incrby { key, increment }) 21 | } 22 | 23 | pub fn apply(self, db: &mut Db) -> Result { 24 | match db.get_mut(&self.key) { 25 | Some(structure) => { 26 | match structure { 27 | Structure::String(str) => { 28 | match str.parse::() { 29 | Ok(mut num) => { 30 | num += self.increment; 31 | *str = num.to_string(); 32 | Ok(Frame::Integer(num)) 33 | }, 34 | Err(_) => { 35 | let f = "ERR value is not an integer or out of range"; 36 | Ok(Frame::Error(f.to_string())) 37 | } 38 | } 39 | }, 40 | _ => { 41 | let f = "ERR Operation against a key holding the wrong kind of value"; 42 | Ok(Frame::Error(f.to_string())) 43 | } 44 | } 45 | }, 46 | None => { 47 | let new_value = self.increment; 48 | db.insert(self.key.clone(), Structure::String(new_value.to_string())); 49 | Ok(Frame::Integer(new_value)) 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/cmd/string/mget.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Mget { 5 | keys: Vec, 6 | } 7 | 8 | impl Mget { 9 | pub fn parse_from_frame(frame: Frame) -> Result { 10 | let args = frame.get_args_from_index(1); 11 | 12 | Ok(Mget { keys: args }) 13 | } 14 | 15 | pub fn apply(self, db: &mut Db) -> Result { 16 | let mut result = Vec::new(); 17 | for key in self.keys { 18 | match db.get(&key) { 19 | Some(structure) => { 20 | match structure { 21 | Structure::String(str) => result.push(Frame::BulkString(str.to_string())), 22 | _ => result.push(Frame::Null), 23 | } 24 | } 25 | None => result.push(Frame::Null), 26 | } 27 | } 28 | Ok(Frame::Array(result)) 29 | } 30 | } -------------------------------------------------------------------------------- /src/cmd/string/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod append; 2 | pub mod set; 3 | pub mod mset; 4 | pub mod get; 5 | pub mod strlen; 6 | pub mod mget; 7 | pub mod incr; 8 | pub mod incrby; 9 | pub mod decrby; 10 | pub mod decr; -------------------------------------------------------------------------------- /src/cmd/string/mset.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Mset { 5 | key_vals: Vec<(String, String)>, 6 | } 7 | 8 | impl Mset { 9 | pub fn parse_from_frame(frame: Frame) -> Result { 10 | 11 | let args = frame.get_args_from_index(1); 12 | 13 | if args.len() % 2 != 0 { 14 | return Err(Error::msg("ERR wrong number of arguments for 'mset' command")); 15 | } 16 | 17 | let mut key_vals = Vec::new(); 18 | 19 | for i in (0..args.len()).step_by(2) { 20 | let key = args[i].to_string(); 21 | let val = args[i + 1].to_string(); 22 | key_vals.push((key, val)); 23 | } 24 | 25 | Ok(Mset { key_vals }) 26 | } 27 | 28 | pub fn apply(self, db: &mut Db) -> Result { 29 | for (key, val) in self.key_vals { 30 | db.insert(key, Structure::String(val)); 31 | } 32 | Ok(Frame::Ok) 33 | } 34 | } -------------------------------------------------------------------------------- /src/cmd/string/set.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::{db::{Db, Structure}, frame::Frame}; 4 | 5 | pub struct Set { 6 | key: String, 7 | val: String, 8 | ttl: Option 9 | } 10 | 11 | impl Set { 12 | 13 | pub fn parse_from_frame(frame: Frame) -> Result{ 14 | 15 | let key = frame.get_arg(1); 16 | let val = frame.get_arg(2); 17 | 18 | if key.is_none() { 19 | return Err(Error::msg("ERR wrong number of arguments for 'set' command")); 20 | } 21 | 22 | if val.is_none() { 23 | return Err(Error::msg("ERR wrong number of arguments for 'set' command")); 24 | } 25 | 26 | let fianl_key = key.unwrap().to_string(); // 键 27 | let final_val = val.unwrap().to_string(); // 值 28 | 29 | let args = frame.get_args(); 30 | 31 | // 检测 EX 和 PX 是否存在 32 | let mut ttl: Option = None; // 默认 ttl 为 0 33 | for (idx, item) in args.iter().enumerate() { 34 | if idx > 2 { // 从第三个参数开始检查,因为前两个是 key 和 val 35 | match item.as_str() { 36 | "EX" | "PX" => { 37 | if let Some(ttl_value) = args.get(idx + 1) { 38 | ttl = match item.as_str() { 39 | "EX" => Some(ttl_value.parse::()? * 1000), // EX 秒 40 | "PX" => Some(ttl_value.parse::()?), // PX 毫秒 41 | _ => None, 42 | }; 43 | break; // 找到 ttl 后退出循环 44 | } 45 | }, 46 | _ => continue, 47 | } 48 | } 49 | } 50 | 51 | Ok(Set { 52 | key: fianl_key, 53 | val: final_val, 54 | ttl: ttl 55 | }) 56 | } 57 | 58 | pub fn apply(self,db: &mut Db) -> Result { 59 | db.insert(self.key.clone(), Structure::String(self.val)); 60 | if let Some(ttl) = self.ttl { 61 | db.expire(self.key.clone(), ttl); 62 | } 63 | Ok(Frame::Ok) 64 | } 65 | } -------------------------------------------------------------------------------- /src/cmd/string/strlen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use crate::{db::{Db, Structure}, frame::Frame}; 3 | 4 | pub struct Strlen { 5 | key: String, 6 | } 7 | 8 | impl Strlen { 9 | 10 | pub fn parse_from_frame(frame: Frame) -> Result { 11 | let key = frame.get_arg(1); 12 | 13 | if key.is_none() { 14 | return Err(Error::msg("ERR wrong number of arguments for 'strlen' command")); 15 | } 16 | 17 | let final_key = key.unwrap().to_string(); 18 | 19 | Ok(Strlen { 20 | key: final_key, 21 | }) 22 | } 23 | 24 | pub fn apply(self, db: &mut Db) -> Result { 25 | let result_structure = db.get(&self.key); 26 | match result_structure { 27 | Some(structure) => { 28 | match structure { 29 | Structure::String(value) => { 30 | Ok(Frame::Integer(value.len() as i64)) 31 | }, 32 | _ => { 33 | Ok(Frame::Error("Type parsing error".to_string())) 34 | } 35 | } 36 | }, 37 | None => { 38 | Ok(Frame::Integer(0)) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/cmd/unknown.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::frame::Frame; 4 | 5 | pub struct Unknown { 6 | command_name: String, 7 | args: String 8 | } 9 | 10 | impl Unknown { 11 | 12 | pub fn parse_from_frame(frame: Frame) -> Result { 13 | 14 | let command_name = match frame.get_arg(0) { 15 | Some(name) => name.to_string(), 16 | None => return Err(Error::msg("Failed to get command name")), 17 | }; 18 | 19 | let mut args = String::new(); 20 | 21 | for arg in frame.get_args().iter().skip(1) { 22 | args.push_str(arg); 23 | args.push(' '); 24 | } 25 | 26 | if !args.is_empty() { 27 | args.pop(); 28 | } 29 | 30 | Ok(Unknown { 31 | command_name, 32 | args 33 | }) 34 | } 35 | 36 | pub fn apply(self) -> Result { 37 | Ok(Frame::Error(format!("ERR unknown command `{}`, with args beginning with: `{}`", self.command_name, self.args))) 38 | } 39 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | pub mod cmd; 3 | pub mod server_handler; 4 | pub mod command; 5 | pub mod server; 6 | pub mod frame; 7 | pub mod db; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rudis_server::args::Args; 3 | use rudis_server::db::DbGuard; 4 | use rudis_server::server::Server; 5 | use std::sync::Arc; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | 10 | let args = Arc::new(Args::parse()); 11 | std::env::set_var("RUST_LOG", &args.loglevel); 12 | env_logger::init(); 13 | 14 | let db_guard = Arc::new(DbGuard::new(args.clone())); 15 | let server = Server::new(args.clone(), db_guard); 16 | server.start().await; 17 | } -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::process::id; 2 | use std::sync::Arc; 3 | use tokio::net::TcpListener; 4 | 5 | use crate::args::Args; 6 | use crate::db::DbGuard; 7 | use crate::server_handler::ServerHandler; 8 | 9 | pub struct Server { 10 | args: Arc, 11 | db_guard: Arc, 12 | } 13 | 14 | impl Server { 15 | 16 | pub fn new(args: Arc, db_guard: Arc) -> Self { 17 | Server { args, db_guard } 18 | } 19 | 20 | pub async fn start(&self) { 21 | match TcpListener::bind(format!("{}:{}", self.args.bind, self.args.port)).await { 22 | Ok(listener) => { 23 | self.server_info(); 24 | log::info!("Server initialized"); 25 | log::info!("Ready to accept connections"); 26 | loop { 27 | match listener.accept().await { 28 | Ok((stream, _address)) => { 29 | let mut handler = ServerHandler::new(self.db_guard.clone(), stream, self.args.clone()); 30 | tokio::spawn(async move { 31 | handler.handle().await; 32 | }); 33 | } 34 | Err(e) => { 35 | log::error!("Failed to accept connection: {}", e); 36 | } 37 | } 38 | } 39 | } 40 | Err(_e) => { 41 | log::error!("Failed to bind to address {}:{}", self.args.bind, self.args.port); 42 | std::process::exit(1); 43 | } 44 | } 45 | } 46 | 47 | fn server_info(&self) { 48 | let pid = id(); 49 | let version = env!("CARGO_PKG_VERSION"); 50 | let pattern = format!( 51 | r#" 52 | /\_____/\ 53 | / o o \ Rudis {} 54 | ( == ^ == ) 55 | ) ( Bind: {} PID: {} 56 | ( ) 57 | ( ( ) ( ) ) 58 | (__(__)___(__)__) 59 | "#, version, self.args.port, pid); 60 | println!("{}", pattern); 61 | } 62 | } -------------------------------------------------------------------------------- /tests/test_performance.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test_performance { 3 | 4 | use redis::{Client, Commands, Connection}; 5 | 6 | fn setup() -> Connection { 7 | let client = Client::open("redis://127.0.0.1:6379/").unwrap(); 8 | client.get_connection().unwrap() 9 | } 10 | 11 | #[test] 12 | fn test_set() { 13 | let mut con = setup(); 14 | for i in 0..100000 { 15 | let _: () = con.set(i.to_string(), i.to_string()).unwrap(); 16 | } 17 | } 18 | 19 | #[test] 20 | fn test_get() { 21 | let mut con = setup(); 22 | for _i in 0..100000 { 23 | let _: () = con.get("user").unwrap(); 24 | } 25 | } 26 | } --------------------------------------------------------------------------------