├── .changeset ├── README.md └── config.json ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ ├── check.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── examples ├── memory │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── nodejs-disk │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── r2-node │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── s3-node │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── s3-sign │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts └── s3-workers │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── error.ts │ │ ├── file.ts │ │ ├── index.ts │ │ └── storage.ts │ ├── tests │ │ ├── file.test.ts │ │ └── storage.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── memory │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tests │ │ └── index.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── node-disk │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tests │ │ ├── fixture │ │ │ └── sample1.txt │ │ └── index.test.ts │ ├── tsconfig.json │ └── vitest.config.ts └── s3 │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── index.ts │ └── storage.ts │ ├── tests │ └── index.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json └── website ├── .gitignore ├── docs ├── .vitepress │ └── config.ts └── index.md └── package.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "sor4chi/hono-storage" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "public", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | module.exports = { 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/recommended", 10 | "plugin:import/typescript", 11 | "prettier", 12 | ], 13 | plugins: ["@typescript-eslint", "unused-imports"], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | project, 17 | }, 18 | settings: { 19 | "import/resolver": { 20 | typescript: { 21 | project, 22 | }, 23 | }, 24 | }, 25 | rules: { 26 | "import/order": [ 27 | "warn", 28 | { 29 | groups: [ 30 | "builtin", 31 | "external", 32 | "internal", 33 | "parent", 34 | "sibling", 35 | "index", 36 | "object", 37 | "type", 38 | ], 39 | "newlines-between": "always", 40 | pathGroupsExcludedImportTypes: ["builtin"], 41 | alphabetize: { order: "asc", caseInsensitive: true }, 42 | pathGroups: [], 43 | }, 44 | ], 45 | "@typescript-eslint/no-unused-vars": [ 46 | "warn", 47 | { 48 | argsIgnorePattern: "^_", 49 | varsIgnorePattern: "^_", 50 | caughtErrorsIgnorePattern: "^_", 51 | destructuredArrayIgnorePattern: "^_", 52 | }, 53 | ], 54 | }, 55 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.*"], 56 | }; 57 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sor4chi] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | lint-check: 8 | name: Lint Check 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Repo 12 | uses: actions/checkout@v3 13 | 14 | - name: Launch Turbo Remote Cache Server 15 | uses: dtinth/setup-github-actions-caching-for-turbo@v1.1.0 16 | with: 17 | cache-prefix: turbogha_ 18 | 19 | - name: Setup Node.js 20.x 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 20.x 23 | 24 | - uses: pnpm/action-setup@v2 25 | name: Install pnpm 26 | with: 27 | version: 9 28 | run_install: false 29 | 30 | - name: Get pnpm store directory 31 | shell: bash 32 | run: | 33 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 34 | 35 | - uses: actions/cache@v3 36 | name: Setup pnpm cache 37 | with: 38 | path: ${{ env.STORE_PATH }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install Dependencies 44 | run: pnpm install --frozen-lockfile 45 | 46 | - name: Lint 47 | run: pnpm lint:check 48 | 49 | format-check: 50 | name: Format Check 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout Repo 54 | uses: actions/checkout@v3 55 | 56 | - name: Launch Turbo Remote Cache Server 57 | uses: dtinth/setup-github-actions-caching-for-turbo@v1.1.0 58 | with: 59 | cache-prefix: turbogha_ 60 | 61 | - name: Setup Node.js 20.x 62 | uses: actions/setup-node@v3 63 | with: 64 | node-version: 20.x 65 | 66 | - uses: pnpm/action-setup@v2 67 | name: Install pnpm 68 | with: 69 | version: 9 70 | run_install: false 71 | 72 | - name: Get pnpm store directory 73 | shell: bash 74 | run: | 75 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 76 | 77 | - uses: actions/cache@v3 78 | name: Setup pnpm cache 79 | with: 80 | path: ${{ env.STORE_PATH }} 81 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 82 | restore-keys: | 83 | ${{ runner.os }}-pnpm-store- 84 | 85 | - name: Install Dependencies 86 | run: pnpm install --frozen-lockfile 87 | 88 | - name: Format 89 | run: pnpm format:check 90 | 91 | test: 92 | name: Test 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout Repo 96 | uses: actions/checkout@v3 97 | 98 | - name: Launch Turbo Remote Cache Server 99 | uses: dtinth/setup-github-actions-caching-for-turbo@v1.1.0 100 | with: 101 | cache-prefix: turbogha_ 102 | 103 | - name: Setup Node.js 20.x 104 | uses: actions/setup-node@v3 105 | with: 106 | node-version: 20.x 107 | 108 | - uses: pnpm/action-setup@v2 109 | name: Install pnpm 110 | with: 111 | version: 9 112 | run_install: false 113 | 114 | - name: Get pnpm store directory 115 | shell: bash 116 | run: | 117 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 118 | 119 | - uses: actions/cache@v3 120 | name: Setup pnpm cache 121 | with: 122 | path: ${{ env.STORE_PATH }} 123 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 124 | restore-keys: | 125 | ${{ runner.os }}-pnpm-store- 126 | 127 | - name: Install Dependencies 128 | run: pnpm install --frozen-lockfile 129 | 130 | - name: Test 131 | run: pnpm test 132 | 133 | build: 134 | name: Build 135 | runs-on: ubuntu-latest 136 | steps: 137 | - name: Checkout Repo 138 | uses: actions/checkout@v3 139 | 140 | - name: Launch Turbo Remote Cache Server 141 | uses: dtinth/setup-github-actions-caching-for-turbo@v1.1.0 142 | with: 143 | cache-prefix: turbogha_ 144 | 145 | - name: Setup Node.js 20.x 146 | uses: actions/setup-node@v3 147 | with: 148 | node-version: 20.x 149 | 150 | - uses: pnpm/action-setup@v2 151 | name: Install pnpm 152 | with: 153 | version: 9 154 | run_install: false 155 | 156 | - name: Get pnpm store directory 157 | shell: bash 158 | run: | 159 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 160 | 161 | - uses: actions/cache@v3 162 | name: Setup pnpm cache 163 | with: 164 | path: ${{ env.STORE_PATH }} 165 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 166 | restore-keys: | 167 | ${{ runner.os }}-pnpm-store- 168 | 169 | - name: Install Dependencies 170 | run: pnpm install --frozen-lockfile 171 | 172 | - name: Build 173 | run: pnpm build 174 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js 20.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | always-auth: true 23 | scope: "@hono-storage" 24 | 25 | - uses: pnpm/action-setup@v2 26 | name: Install pnpm 27 | with: 28 | version: 9 29 | run_install: false 30 | 31 | - name: Get pnpm store directory 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 35 | 36 | - uses: actions/cache@v3 37 | name: Setup pnpm cache 38 | with: 39 | path: ${{ env.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install Dependencies 45 | run: pnpm install --frozen-lockfile 46 | 47 | - name: Create Release Pull Request or Publish to npm 48 | id: changesets 49 | uses: changesets/action@v1 50 | with: 51 | publish: pnpm release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.export = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sor4chi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage 2 | 3 | Hono Storage is a storage helper for [Hono](https://github.com/honojs/hono), this module is like [multer](https://github.com/expressjs/multer) in expressjs. 4 | 5 | > [!WARNING] 6 | > This is a work in progress. The code is not yet ready for production use. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm install @hono-storage/core 12 | ``` 13 | 14 | ### Helper 15 | 16 | you can use helper to install storage for Hono. 17 | 18 | ```bash 19 | npm install @hono-storage/node-disk # for nodejs disk storage 20 | npm install @hono-storage/memory # for in-memory storage 21 | npm install @hono-storage/s3 # for s3 storage (or r2 storage) 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```ts 27 | import { Hono } from "hono"; 28 | 29 | const app = new Hono(); 30 | 31 | const storage = // your storage, see below 32 | 33 | app.post("/upload/single", storage.single("image"), (c) => c.text("OK")); 34 | app.post("/upload/multiple", storage.multiple("pictures"), (c) => c.text("OK")); 35 | app.post( 36 | "/upload/field", 37 | storage.fields({ 38 | image: { type: "single" }, 39 | pictures: { type: "multiple", maxCount: 2 }, 40 | }), 41 | (c) => c.text("OK"), 42 | ); 43 | 44 | // and you can get parsed formData easily 45 | app.post("/upload/vars", storage.single("image"), (c) => { 46 | const { image } = c.var.files; 47 | // do something with file 48 | return c.text("OK"); 49 | }); 50 | 51 | // serve app 52 | ``` 53 | 54 | ### Storage 55 | 56 |
57 | Normal Storage 58 | 59 | ```ts 60 | import { HonoStorage } from "@hono-storage/core"; 61 | 62 | const storage = new HonoStorage({ 63 | storage: (c, files) => { 64 | // do something with the files, eg, upload to s3, or save to local, etc. 65 | }, 66 | }); 67 | ``` 68 |
69 | 70 |
71 | Node.js Disk Storage 72 | 73 | ```ts 74 | import { HonoDiskStorage } from "@hono-storage/node-disk"; 75 | 76 | const storage = new HonoDiskStorage({ 77 | dest: "./uploads", 78 | filename: (c, file) => `${file.originalname}-${new Date().getTime()}.${file.extension}`, 79 | }); 80 | ``` 81 | 82 |
83 | 84 |
85 | In-Memory Storage 86 | 87 | ```ts 88 | import { HonoMemoryStorage } from "@hono-storage/memory"; 89 | 90 | const storage = new HonoMemoryStorage({ 91 | key: (c, file) => `${file.originalname}-${new Date().getTime()}`, 92 | }); 93 | ``` 94 | 95 |
96 | 97 |
98 | S3 Storage (Also R2) 99 | 100 | ```ts 101 | import { S3Client } from "@aws-sdk/client-s3"; 102 | import { HonoS3Storage } from "@hono-storage/s3"; 103 | 104 | /** if you use S3 */ 105 | const client = new S3Client({ 106 | region: "[your-bucket-region]", 107 | credentials: { 108 | accessKeyId: AWS_ACCESS_KEY_ID, 109 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 110 | }, 111 | }); 112 | 113 | /** if you use R2 */ 114 | const client = new S3Client({ 115 | region: "auto", 116 | endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, 117 | credentials: { 118 | accessKeyId: ACCESS_KEY_ID, 119 | secretAccessKey: SECRET_ACCESS_KEY, 120 | }, 121 | }); 122 | 123 | const storage = new HonoS3Storage({ 124 | key: (_, file) => `${file.originalname}-${new Date().getTime()}.${file.extension}`, 125 | bucket: "[your-bucket-name]", 126 | client, 127 | }); 128 | ``` 129 | 130 |
131 | 132 | 133 | You want to find more? Check out the [examples](./examples)! 134 | 135 | ## License 136 | 137 | [MIT](./LICENSE) 138 | 139 | ## Contributing 140 | 141 | This project is open for contributions. Feel free to open an issue or a pull request! 142 | -------------------------------------------------------------------------------- /examples/memory/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | uploads 3 | -------------------------------------------------------------------------------- /examples/memory/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/memory-example 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/memory@0.0.13 9 | 10 | ## 0.0.12 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/memory@0.0.12 16 | 17 | ## 0.0.11 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 22 | - @hono-storage/memory@0.0.11 23 | 24 | ## 0.0.10 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 29 | - @hono-storage/memory@0.0.10 30 | 31 | ## 0.0.9 32 | 33 | ### Patch Changes 34 | 35 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 36 | 37 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 38 | - @hono-storage/memory@0.0.9 39 | 40 | ## 0.0.8 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 45 | - @hono-storage/memory@0.0.8 46 | 47 | ## 0.0.7 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies []: 52 | - @hono-storage/memory@0.0.7 53 | 54 | ## 0.0.6 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies []: 59 | - @hono-storage/memory@0.0.6 60 | 61 | ## 0.0.5 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies []: 66 | - @hono-storage/memory@0.0.5 67 | 68 | ## 0.0.4 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies []: 73 | - @hono-storage/memory@0.0.4 74 | 75 | ## 0.0.3 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies []: 80 | - @hono-storage/memory@0.0.3 81 | 82 | ## 0.0.2 83 | 84 | ### Patch Changes 85 | 86 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 87 | 88 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 89 | - @hono-storage/memory@0.0.2 90 | 91 | ## 0.0.1 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [[`845d497`](https://github.com/sor4chi/hono-storage/commit/845d497f8f0c604dd81839150cdc7c8de5104c66)]: 96 | - @hono-storage/memory@0.0.1 97 | -------------------------------------------------------------------------------- /examples/memory/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for In-Memory 2 | 3 | ```bash 4 | pnpm i 5 | pnpm start 6 | ``` 7 | -------------------------------------------------------------------------------- /examples/memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/memory-example", 3 | "private": true, 4 | "version": "0.0.13", 5 | "scripts": { 6 | "start": "tsx src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@hono/node-server": "^1.1.0", 10 | "@hono-storage/memory": "workspace:*", 11 | "hono": "^4.3.2" 12 | }, 13 | "devDependencies": { 14 | "tsx": "^3.12.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/memory/src/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { HonoMemoryStorage } from "@hono-storage/memory"; 3 | import { Hono } from "hono"; 4 | 5 | const app = new Hono(); 6 | const storage = new HonoMemoryStorage({ 7 | key: (c, file) => `${file.originalname}-${new Date().getTime()}`, 8 | }); 9 | 10 | app.post("/", storage.single("file"), (c) => c.text("OK")); 11 | app.get("/show/:key", async (c) => { 12 | const key = c.req.param("key"); 13 | const file = storage.buffer.get(key); 14 | if (!file) { 15 | return c.text("File not found"); 16 | } 17 | return c.json({ 18 | key, 19 | size: file.size, 20 | type: file.type, 21 | name: file.name, 22 | content: await file.text(), 23 | }); 24 | }); 25 | app.get("/list", (c) => { 26 | return c.json(storage.buffer.forEach((file) => file.name)); 27 | }); 28 | 29 | serve(app); 30 | -------------------------------------------------------------------------------- /examples/nodejs-disk/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | uploads 3 | -------------------------------------------------------------------------------- /examples/nodejs-disk/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/node-disk-example 2 | 3 | ## 0.0.16 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/node-disk@0.0.16 9 | 10 | ## 0.0.15 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/node-disk@0.0.15 16 | 17 | ## 0.0.14 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`e5ed56e`](https://github.com/sor4chi/hono-storage/commit/e5ed56e787c81986102fd59d1d5ad951fe0ac64b)]: 22 | - @hono-storage/node-disk@0.0.14 23 | 24 | ## 0.0.13 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 29 | - @hono-storage/node-disk@0.0.13 30 | 31 | ## 0.0.12 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 36 | - @hono-storage/node-disk@0.0.12 37 | 38 | ## 0.0.11 39 | 40 | ### Patch Changes 41 | 42 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 43 | 44 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 45 | - @hono-storage/node-disk@0.0.11 46 | 47 | ## 0.0.10 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 52 | - @hono-storage/node-disk@0.0.10 53 | 54 | ## 0.0.9 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies []: 59 | - @hono-storage/node-disk@0.0.9 60 | 61 | ## 0.0.8 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies []: 66 | - @hono-storage/node-disk@0.0.8 67 | 68 | ## 0.0.7 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies []: 73 | - @hono-storage/node-disk@0.0.7 74 | 75 | ## 0.0.6 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies []: 80 | - @hono-storage/node-disk@0.0.6 81 | 82 | ## 0.0.5 83 | 84 | ### Patch Changes 85 | 86 | - Updated dependencies []: 87 | - @hono-storage/node-disk@0.0.5 88 | 89 | ## 0.0.4 90 | 91 | ### Patch Changes 92 | 93 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 94 | 95 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 96 | - @hono-storage/node-disk@0.0.4 97 | 98 | ## 0.0.3 99 | 100 | ### Patch Changes 101 | 102 | - Updated dependencies []: 103 | - @hono-storage/node-disk@0.0.3 104 | 105 | ## 0.0.2 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies [[`da24913`](https://github.com/sor4chi/hono-storage/commit/da249130275d6a2c2827f17cdd1778bfb2fe34f9)]: 110 | - @hono-storage/node-disk@0.0.2 111 | 112 | ## 0.0.1 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies [[`472a0a3`](https://github.com/sor4chi/hono-storage/commit/472a0a39cd750b3483d01c5b72bec816c7b8cac9)]: 117 | - @hono-storage/node-disk@0.0.1 118 | -------------------------------------------------------------------------------- /examples/nodejs-disk/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for Node.js Disk 2 | 3 | ```bash 4 | pnpm i 5 | pnpm start 6 | ``` 7 | -------------------------------------------------------------------------------- /examples/nodejs-disk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/node-disk-example", 3 | "private": true, 4 | "version": "0.0.16", 5 | "scripts": { 6 | "start": "tsx src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@hono/node-server": "^1.1.0", 10 | "@hono-storage/node-disk": "workspace:*", 11 | "hono": "^4.3.2" 12 | }, 13 | "devDependencies": { 14 | "tsx": "^3.12.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nodejs-disk/src/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { HonoDiskStorage } from "@hono-storage/node-disk"; 3 | import { Hono } from "hono"; 4 | 5 | const app = new Hono(); 6 | const storage = new HonoDiskStorage({ 7 | dest: "./uploads", 8 | filename: (_, file) => `${file.originalname}-${Date.now()}.${file.extension}`, 9 | }); 10 | 11 | app.post("/", storage.single("file"), (c) => c.text("OK")); 12 | 13 | serve(app); 14 | -------------------------------------------------------------------------------- /examples/r2-node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /examples/r2-node/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/r2-node-example 2 | 3 | ## 0.0.12 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/s3@0.0.13 9 | 10 | ## 0.0.11 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/s3@0.0.12 16 | 17 | ## 0.0.10 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 22 | - @hono-storage/s3@0.0.11 23 | 24 | ## 0.0.9 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 29 | - @hono-storage/s3@0.0.10 30 | 31 | ## 0.0.8 32 | 33 | ### Patch Changes 34 | 35 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 36 | 37 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 38 | - @hono-storage/s3@0.0.9 39 | 40 | ## 0.0.7 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 45 | - @hono-storage/s3@0.0.8 46 | 47 | ## 0.0.6 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [[`ad5332b`](https://github.com/sor4chi/hono-storage/commit/ad5332b6689ad1baeba70406d732d81623779e97)]: 52 | - @hono-storage/s3@0.0.7 53 | 54 | ## 0.0.5 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies []: 59 | - @hono-storage/s3@0.0.6 60 | 61 | ## 0.0.4 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies []: 66 | - @hono-storage/s3@0.0.5 67 | 68 | ## 0.0.3 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies []: 73 | - @hono-storage/s3@0.0.4 74 | 75 | ## 0.0.2 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies []: 80 | - @hono-storage/s3@0.0.3 81 | 82 | ## 0.0.1 83 | 84 | ### Patch Changes 85 | 86 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 87 | 88 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 89 | - @hono-storage/s3@0.0.2 90 | -------------------------------------------------------------------------------- /examples/r2-node/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for Cloudflare R2 in Node.js 2 | 3 | ```bash 4 | touch .env 5 | ``` 6 | 7 | Fill in the following environment variables: 8 | 9 | ```env 10 | ACCESS_KEY_ID= 11 | SECRET_ACCESS_KEY= 12 | ACCOUNT_ID= 13 | ``` 14 | 15 | ```bash 16 | pnpm i 17 | pnpm start 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/r2-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/r2-node-example", 3 | "private": true, 4 | "version": "0.0.12", 5 | "scripts": { 6 | "start": "tsx src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@aws-sdk/client-s3": "^3.428.0", 10 | "@hono-storage/s3": "workspace:*", 11 | "@hono/node-server": "^1.1.0", 12 | "dotenv": "^16.3.1", 13 | "hono": "^4.3.2" 14 | }, 15 | "devDependencies": { 16 | "tsx": "^3.12.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/r2-node/src/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { serve } from "@hono/node-server"; 3 | import { HonoS3Storage } from "@hono-storage/s3"; 4 | import { config } from "dotenv"; 5 | import { Hono } from "hono"; 6 | 7 | config(); 8 | 9 | const app = new Hono(); 10 | 11 | if ( 12 | !process.env.ACCESS_KEY_ID || 13 | !process.env.SECRET_ACCESS_KEY || 14 | !process.env.ACCOUNT_ID 15 | ) { 16 | throw new Error("credentials not found"); 17 | } 18 | 19 | const client = new S3Client({ 20 | region: "auto", 21 | endpoint: `https://${process.env.ACCOUNT_ID}.r2.cloudflarestorage.com`, 22 | credentials: { 23 | accessKeyId: process.env.ACCESS_KEY_ID, 24 | secretAccessKey: process.env.SECRET_ACCESS_KEY, 25 | }, 26 | }); 27 | 28 | const storage = new HonoS3Storage({ 29 | key: (_, file) => 30 | `${file.originalname}-${new Date().getTime()}.${file.extension}`, 31 | bucket: "hono-storage", 32 | client, 33 | }); 34 | 35 | app.post("/", storage.single("file"), (c) => c.text("OK")); 36 | 37 | serve(app); 38 | -------------------------------------------------------------------------------- /examples/s3-node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /examples/s3-node/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/s3-node-example 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/s3@0.0.13 9 | 10 | ## 0.0.12 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/s3@0.0.12 16 | 17 | ## 0.0.11 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 22 | - @hono-storage/s3@0.0.11 23 | 24 | ## 0.0.10 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 29 | - @hono-storage/s3@0.0.10 30 | 31 | ## 0.0.9 32 | 33 | ### Patch Changes 34 | 35 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 36 | 37 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 38 | - @hono-storage/s3@0.0.9 39 | 40 | ## 0.0.8 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 45 | - @hono-storage/s3@0.0.8 46 | 47 | ## 0.0.7 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [[`ad5332b`](https://github.com/sor4chi/hono-storage/commit/ad5332b6689ad1baeba70406d732d81623779e97)]: 52 | - @hono-storage/s3@0.0.7 53 | 54 | ## 0.0.6 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies []: 59 | - @hono-storage/s3@0.0.6 60 | 61 | ## 0.0.5 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies []: 66 | - @hono-storage/s3@0.0.5 67 | 68 | ## 0.0.4 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies []: 73 | - @hono-storage/s3@0.0.4 74 | 75 | ## 0.0.3 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies []: 80 | - @hono-storage/s3@0.0.3 81 | 82 | ## 0.0.2 83 | 84 | ### Patch Changes 85 | 86 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 87 | 88 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 89 | - @hono-storage/s3@0.0.2 90 | 91 | ## 0.0.1 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [[`ec74110`](https://github.com/sor4chi/hono-storage/commit/ec741102219a960c5a0e8317b0eda3ce4e3f4a14)]: 96 | - @hono-storage/s3@0.0.1 97 | -------------------------------------------------------------------------------- /examples/s3-node/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for AWS S3 in Node.js 2 | 3 | ```bash 4 | touch .env 5 | ``` 6 | 7 | Fill in the following environment variables: 8 | 9 | ```env 10 | AWS_ACCESS_KEY_ID= 11 | AWS_SECRET_ACCESS_KEY= 12 | ``` 13 | 14 | ```bash 15 | pnpm i 16 | pnpm start 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/s3-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/s3-node-example", 3 | "private": true, 4 | "version": "0.0.13", 5 | "scripts": { 6 | "start": "tsx src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@aws-sdk/client-s3": "^3.428.0", 10 | "@hono-storage/s3": "workspace:*", 11 | "@hono/node-server": "^1.1.0", 12 | "dotenv": "^16.3.1", 13 | "hono": "^4.3.2" 14 | }, 15 | "devDependencies": { 16 | "tsx": "^3.12.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/s3-node/src/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { serve } from "@hono/node-server"; 3 | import { HonoS3Storage } from "@hono-storage/s3"; 4 | import { config } from "dotenv"; 5 | import { Hono } from "hono"; 6 | 7 | config(); 8 | 9 | const app = new Hono(); 10 | 11 | if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) { 12 | throw new Error("AWS credentials not found"); 13 | } 14 | 15 | const client = new S3Client({ 16 | region: "us-east-1", 17 | credentials: { 18 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 19 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 20 | }, 21 | }); 22 | 23 | const storage = new HonoS3Storage({ 24 | key: (_, file) => 25 | `${file.originalname}-${new Date().getTime()}.${file.extension}`, 26 | bucket: "hono-storage", 27 | client, 28 | }); 29 | 30 | app.post("/", storage.single("file"), (c) => c.text("OK")); 31 | 32 | serve(app); 33 | -------------------------------------------------------------------------------- /examples/s3-sign/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /examples/s3-sign/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/s3-sign-example 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/s3@0.0.13 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/s3@0.0.12 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 22 | - @hono-storage/s3@0.0.11 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 29 | - @hono-storage/s3@0.0.10 30 | 31 | ## 0.0.3 32 | 33 | ### Patch Changes 34 | 35 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 36 | 37 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 38 | - @hono-storage/s3@0.0.9 39 | 40 | ## 0.0.2 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 45 | - @hono-storage/s3@0.0.8 46 | 47 | ## 0.0.1 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [[`ad5332b`](https://github.com/sor4chi/hono-storage/commit/ad5332b6689ad1baeba70406d732d81623779e97)]: 52 | - @hono-storage/s3@0.0.7 53 | -------------------------------------------------------------------------------- /examples/s3-sign/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for AWS S3 with signed URL in Node.js 2 | 3 | ```bash 4 | touch .env 5 | ``` 6 | 7 | Fill in the following environment variables: 8 | 9 | ```env 10 | AWS_ACCESS_KEY_ID= 11 | AWS_SECRET_ACCESS_KEY= 12 | ``` 13 | 14 | ```bash 15 | pnpm i 16 | pnpm start 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/s3-sign/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/s3-sign-example", 3 | "private": true, 4 | "version": "0.0.7", 5 | "scripts": { 6 | "start": "tsx src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@aws-sdk/client-s3": "^3.428.0", 10 | "@hono-storage/s3": "workspace:*", 11 | "@hono/node-server": "^1.1.0", 12 | "dotenv": "^16.3.1", 13 | "hono": "^4.3.2" 14 | }, 15 | "devDependencies": { 16 | "tsx": "^3.12.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/s3-sign/src/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { serve } from "@hono/node-server"; 3 | import { HonoS3Storage } from "@hono-storage/s3"; 4 | import { config } from "dotenv"; 5 | import { Hono } from "hono"; 6 | import { html } from "hono/html"; 7 | 8 | config(); 9 | 10 | const app = new Hono(); 11 | 12 | if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) { 13 | throw new Error("AWS credentials not found"); 14 | } 15 | 16 | const client = new S3Client({ 17 | region: "us-east-1", 18 | credentials: { 19 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 20 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 21 | }, 22 | }); 23 | 24 | const storage = new HonoS3Storage({ 25 | key: (_, file) => 26 | `${file.originalname}-${new Date().getTime()}.${file.extension}`, 27 | bucket: "hono-storage", 28 | client, 29 | }); 30 | 31 | let signedURL = ""; 32 | 33 | app.post( 34 | "/", 35 | storage.single("image", { 36 | sign: { 37 | expiresIn: 60, 38 | }, 39 | }), 40 | (c) => { 41 | signedURL = c.var.signedURLs.image || ""; 42 | return c.html(html` 43 | Back 44 |

Image uploaded successfully

45 | ${signedURL} 46 | `); 47 | }, 48 | ); 49 | 50 | app.get("/", (c) => 51 | c.html(html` 52 |
53 | 54 | 55 |
56 | 57 | `), 58 | ); 59 | 60 | serve(app); 61 | -------------------------------------------------------------------------------- /examples/s3-workers/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | -------------------------------------------------------------------------------- /examples/s3-workers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/s3-workers-example 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies []: 8 | - @hono-storage/s3@0.0.13 9 | 10 | ## 0.0.12 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies []: 15 | - @hono-storage/s3@0.0.12 16 | 17 | ## 0.0.11 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 22 | - @hono-storage/s3@0.0.11 23 | 24 | ## 0.0.10 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 29 | - @hono-storage/s3@0.0.10 30 | 31 | ## 0.0.9 32 | 33 | ### Patch Changes 34 | 35 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 36 | 37 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 38 | - @hono-storage/s3@0.0.9 39 | 40 | ## 0.0.8 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 45 | - @hono-storage/s3@0.0.8 46 | 47 | ## 0.0.7 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [[`ad5332b`](https://github.com/sor4chi/hono-storage/commit/ad5332b6689ad1baeba70406d732d81623779e97)]: 52 | - @hono-storage/s3@0.0.7 53 | 54 | ## 0.0.6 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies []: 59 | - @hono-storage/s3@0.0.6 60 | 61 | ## 0.0.5 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies []: 66 | - @hono-storage/s3@0.0.5 67 | 68 | ## 0.0.4 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies []: 73 | - @hono-storage/s3@0.0.4 74 | 75 | ## 0.0.3 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies []: 80 | - @hono-storage/s3@0.0.3 81 | 82 | ## 0.0.2 83 | 84 | ### Patch Changes 85 | 86 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 87 | 88 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 89 | - @hono-storage/s3@0.0.2 90 | 91 | ## 0.0.1 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [[`ec74110`](https://github.com/sor4chi/hono-storage/commit/ec741102219a960c5a0e8317b0eda3ce4e3f4a14)]: 96 | - @hono-storage/s3@0.0.1 97 | -------------------------------------------------------------------------------- /examples/s3-workers/README.md: -------------------------------------------------------------------------------- 1 | # Hono Storage for AWS S3 in Cloudflare Workers 2 | 3 | ```bash 4 | touch .dev.vars 5 | ``` 6 | 7 | Fill in the following environment variables: 8 | 9 | ```env 10 | AWS_ACCESS_KEY_ID= 11 | AWS_SECRET_ACCESS_KEY= 12 | ``` 13 | 14 | ```bash 15 | pnpm i 16 | pnpm dev 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/s3-workers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/s3-workers-example", 3 | "private": true, 4 | "version": "0.0.13", 5 | "scripts": { 6 | "dev": "wrangler dev src/index.ts", 7 | "deploy": "wrangler deploy --minify src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@aws-sdk/client-s3": "^3.428.0", 11 | "@hono-storage/s3": "workspace:*", 12 | "hono": "^4.3.2" 13 | }, 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20230914.0", 16 | "wrangler": "^3.9.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/s3-workers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { HonoS3Storage } from "@hono-storage/s3"; 3 | import { Hono } from "hono"; 4 | 5 | const app = new Hono(); 6 | const client = (accessKeyId: string, secretAccessKey: string) => 7 | new S3Client({ 8 | region: "us-east-1", 9 | credentials: { 10 | accessKeyId, 11 | secretAccessKey, 12 | }, 13 | }); 14 | 15 | const storage = new HonoS3Storage({ 16 | key: (_, file) => 17 | `${file.originalname}-${new Date().getTime()}.${file.extension}`, 18 | bucket: "hono-storage", 19 | client: (c) => client(c.env.AWS_ACCESS_KEY_ID, c.env.AWS_SECRET_ACCESS_KEY), 20 | }); 21 | 22 | app.post("/", storage.single("file"), (c) => c.text("OK")); 23 | 24 | export default app; 25 | -------------------------------------------------------------------------------- /examples/s3-workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "lib": [ 9 | "esnext" 10 | ], 11 | "types": [ 12 | "@cloudflare/workers-types" 13 | ], 14 | "jsx": "react-jsx", 15 | "jsxImportSource": "hono/jsx" 16 | }, 17 | } -------------------------------------------------------------------------------- /examples/s3-workers/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "s3-workers" 2 | compatibility_date = "2023-01-01" 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "private": true, 4 | "scripts": { 5 | "lint": "turbo lint", 6 | "lint:check": "turbo lint:check", 7 | "format": "turbo format", 8 | "format:check": "turbo format:check", 9 | "build": "turbo build", 10 | "test": "turbo test", 11 | "release": "pnpm build && changeset publish" 12 | }, 13 | "devDependencies": { 14 | "@changesets/changelog-github": "^0.4.8", 15 | "@changesets/cli": "^2.26.2", 16 | "@typescript-eslint/eslint-plugin": "^6.6.0", 17 | "@typescript-eslint/parser": "^6.6.0", 18 | "eslint-config-prettier": "^9.0.0", 19 | "eslint-import-resolver-typescript": "^3.6.0", 20 | "eslint-plugin-import": "^2.28.1", 21 | "eslint-plugin-react-hooks": "^4.6.0", 22 | "eslint-plugin-unused-imports": "^3.0.0", 23 | "prettier": "^3.0.3", 24 | "tsup": "^7.2.0", 25 | "tsx": "^3.12.2", 26 | "turbo": "^1.10.13", 27 | "typescript": "^5.2.2", 28 | "vitest": "^0.34.5", 29 | "vitest-environment-miniflare": "^2.14.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/core 2 | 3 | ## 0.0.14 4 | 5 | ### Patch Changes 6 | 7 | - [#71](https://github.com/sor4chi/hono-storage/pull/71) [`1649e17`](https://github.com/sor4chi/hono-storage/commit/1649e172335fd7780f04412f72769ab7d991a790) Thanks [@sngmn451](https://github.com/sngmn451)! - feat: preserve File type property in HonoStorageFile 8 | When a File is processed by the storage middleware, the `type` property from the original File object is now correctly inherited by HonoStorageFile. This ensures the file's content-type is maintained during file uploads. 9 | 10 | ## 0.0.13 11 | 12 | ### Patch Changes 13 | 14 | - [#65](https://github.com/sor4chi/hono-storage/pull/65) [`474a669`](https://github.com/sor4chi/hono-storage/commit/474a669a8f43156aafa58173390504d355ff1b7f) Thanks [@sor4chi](https://github.com/sor4chi)! - fix: remove `File` (`@web-std/file`) polyfill from `@hono-storage/core` package 15 | 16 | This changes means that **stop Node.js v18 support** for `@hono-storage/core` package. 17 | 18 | `@web-std/file` is a polyfill for the `File` class, but for web compatibility, it's not necessary to adapt the `File` class to Node.js. 19 | So if you want to Hono Storage to work on older Node.js versions, you can use the `@web-std/file` package manually. 20 | 21 | ## 0.0.12 22 | 23 | ### Patch Changes 24 | 25 | - [#52](https://github.com/sor4chi/hono-storage/pull/52) [`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b) Thanks [@sor4chi](https://github.com/sor4chi)! - Add require exports field for cjs usecase 26 | 27 | ## 0.0.11 28 | 29 | ### Patch Changes 30 | 31 | - [#51](https://github.com/sor4chi/hono-storage/pull/51) [`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917) Thanks [@sor4chi](https://github.com/sor4chi)! - Move Hono to peerDependencies. Hono Storage now has compatibility with Hono v3.8 or later. 32 | 33 | ## 0.0.10 34 | 35 | ### Patch Changes 36 | 37 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 38 | 39 | ## 0.0.9 40 | 41 | ### Patch Changes 42 | 43 | - [`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3) Thanks [@sor4chi](https://github.com/sor4chi)! - fix package.json 44 | 45 | ## 0.0.8 46 | 47 | ### Patch Changes 48 | 49 | - [#38](https://github.com/sor4chi/hono-storage/pull/38) [`0fffc7f`](https://github.com/sor4chi/hono-storage/commit/0fffc7f76152df882b15398014ca8aa331a6ff12) Thanks [@sor4chi](https://github.com/sor4chi)! - feat: add `field` property to `HonoStorageFile`(HSF). 50 | 51 | ## 0.0.7 52 | 53 | ### Patch Changes 54 | 55 | - [#36](https://github.com/sor4chi/hono-storage/pull/36) [`17d6090`](https://github.com/sor4chi/hono-storage/commit/17d609093ade861c93eaac5418ca0a7debb7bebb) Thanks [@sor4chi](https://github.com/sor4chi)! - Fix: collectly handle storage when one file posted to multiple middleware 56 | 57 | - [`0d257d4`](https://github.com/sor4chi/hono-storage/commit/0d257d42f158bc4485e907d601a6541d0f25a923) Thanks [@sor4chi](https://github.com/sor4chi)! - Breaking Changes: change the way to define multiple middleware option for scalability 58 | 59 | ```ts 60 | // Before 61 | storage.multiple("field", 3); // max 3 files options 62 | 63 | // After 64 | storage.multiple("filed", { maxCount: 3 }); // max 3 files options 65 | ``` 66 | 67 | ## 0.0.6 68 | 69 | ### Patch Changes 70 | 71 | - [#33](https://github.com/sor4chi/hono-storage/pull/33) [`acf1f0d`](https://github.com/sor4chi/hono-storage/commit/acf1f0de6d1c88224182ead9aff3578c5c8842d4) Thanks [@sor4chi](https://github.com/sor4chi)! - `c.var.files` became type-safe 72 | 73 | ```ts 74 | app.post("/single", storage.single("image"), (c) => { 75 | const image = c.var.files.image; // string | File | undefined 76 | }); 77 | 78 | app.post("/multiple", storage.multiple("images"), (c) => { 79 | const images = c.var.files.images; // (string | File)[] 80 | }); 81 | ``` 82 | 83 | - [#30](https://github.com/sor4chi/hono-storage/pull/30) [`6da696f`](https://github.com/sor4chi/hono-storage/commit/6da696f952a6bfeac95725bd077deebba9da8591) Thanks [@sor4chi](https://github.com/sor4chi)! - Breaking Change: Rename `storage.array` to `storage.multiple`. 84 | 85 | ## 0.0.5 86 | 87 | ### Patch Changes 88 | 89 | - [#28](https://github.com/sor4chi/hono-storage/pull/28) [`51fa375`](https://github.com/sor4chi/hono-storage/commit/51fa3752a49ddb7403edb57b0f1a1feaf154978b) Thanks [@sor4chi](https://github.com/sor4chi)! - Breaking change: The argument of `storage.field` function is changed. 90 | 91 | ## Before 92 | 93 | ```ts 94 | storage.field([{ name: "files", maxCount: 3 }, { name: "image" }]); 95 | ``` 96 | 97 | ## After 98 | 99 | ```ts 100 | storage.field({ 101 | files: { type: "multiple", maxCount: 3 }, 102 | image: { type: "single" }, 103 | }); 104 | ``` 105 | 106 | ## 0.0.4 107 | 108 | ### Patch Changes 109 | 110 | - [#24](https://github.com/sor4chi/hono-storage/pull/24) [`07d2d99`](https://github.com/sor4chi/hono-storage/commit/07d2d99cdf20a1694cc03c965da773754ad6fa61) Thanks [@sor4chi](https://github.com/sor4chi)! - ## `storage.field` performance improvement 111 | 112 | `storage.field` is able to upload each fields in parallel now. 113 | 114 | ## 0.0.3 115 | 116 | ### Patch Changes 117 | 118 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 119 | 120 | ## 0.0.2 121 | 122 | ### Patch Changes 123 | 124 | - [#7](https://github.com/sor4chi/hono-storage/pull/7) [`ea1eb7a`](https://github.com/sor4chi/hono-storage/commit/ea1eb7a533b8ba3d08acc80f92b8153a9048bfc9) Thanks [@sor4chi](https://github.com/sor4chi)! - file helper for HonoStorage 125 | 126 | ```ts 127 | import { HonoStorageFile } from "@hono-storage/core"; 128 | 129 | const file = new File([blob], "filename.ext.zip"); 130 | const HSfile = new HonoStorageFile(file); 131 | HSfile.originalname; // => name part of file (filename.ext) 132 | HSfile.extensiton; // => extension part of file (.zip) 133 | ``` 134 | 135 | ## 0.0.1 136 | 137 | ### Patch Changes 138 | 139 | - [`a7ade7f`](https://github.com/sor4chi/hono-storage/commit/a7ade7f3bb67cbf3b70efbdf91e9260043413f16) Thanks [@sor4chi](https://github.com/sor4chi)! - This is Hono Storage, a simple and easy to use file storage library. 140 | 141 | ```bash 142 | npm install @hono-storage/core 143 | ``` 144 | 145 | ```ts 146 | import { HonoStorage } from "@hono-storage/core"; 147 | import { Hono } from "hono"; 148 | 149 | const app = new Hono(); 150 | const storage = new HonoStorage({ 151 | storage: (c, files) => { 152 | // do something with the files, eg, upload to s3, or save to local, etc. 153 | }, 154 | }); 155 | 156 | app.post("/upload/single", storage.single("image"), (c) => c.text("OK")); 157 | app.post("/upload/array", storage.array("pictures"), (c) => c.text("OK")); 158 | app.post( 159 | "/upload/field", 160 | storage.fields([ 161 | { name: "image", maxCount: 1 }, 162 | { name: "pictures", maxCount: 2 }, 163 | ]), 164 | (c) => c.text("OK"), 165 | ); 166 | 167 | // and you can get parsed formData easily 168 | app.post("/upload/vars", storage.single("image"), (c) => { 169 | const { image } = c.get("files"); 170 | // do something with file 171 | return c.text("OK"); 172 | }); 173 | ``` 174 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/core", 3 | "version": "0.0.14", 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint --fix --ext .ts,.tsx src", 10 | "lint:check": "eslint --ext .ts,.tsx src", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\"", 12 | "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", 13 | "build": "tsup ./src/index.ts --format esm,cjs --dts", 14 | "test": "vitest run" 15 | }, 16 | "keywords": [ 17 | "hono" 18 | ], 19 | "files": [ 20 | "dist" 21 | ], 22 | "exports": { 23 | ".": { 24 | "types": "./dist/index.d.ts", 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "author": "sor4chi", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "hono": "^4.3.2" 33 | }, 34 | "peerDependencies": { 35 | "hono": ">=3.8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/error.ts: -------------------------------------------------------------------------------- 1 | export class HonoStorageError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = "HonoStorageError"; 5 | } 6 | } 7 | 8 | export const Errors = { 9 | TooManyFiles: new HonoStorageError("Too many files"), 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/src/file.ts: -------------------------------------------------------------------------------- 1 | type Field = { 2 | name: string; 3 | type: "single" | "multiple"; 4 | }; 5 | 6 | export class HonoStorageFile extends File { 7 | field: Field; 8 | 9 | constructor(file: File, field: Field) { 10 | super([file], file.name, { 11 | type: file.type, 12 | }); 13 | this.field = field; 14 | } 15 | 16 | get originalname(): string { 17 | const name = this.name; 18 | const lastDot = name.lastIndexOf("."); 19 | if (lastDot === -1) { 20 | return name; 21 | } 22 | return name.substring(0, lastDot); 23 | } 24 | 25 | get extension(): string { 26 | const name = this.name; 27 | const lastDot = name.lastIndexOf("."); 28 | if (lastDot === -1) { 29 | return ""; 30 | } 31 | return name.substring(lastDot + 1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./file"; 2 | export * from "./storage"; 3 | export * from "./error"; 4 | -------------------------------------------------------------------------------- /packages/core/src/storage.ts: -------------------------------------------------------------------------------- 1 | import { Errors } from "./error"; 2 | import { HonoStorageFile } from "./file"; 3 | 4 | import type { MiddlewareHandler, Context } from "hono"; 5 | 6 | export type HonoStorageOptions = { 7 | storage?: (c: Context, files: HonoStorageFile[]) => Promise | void; 8 | }; 9 | 10 | export interface BaseFieldSchema { 11 | type: string; 12 | } 13 | 14 | export interface SingleFieldSchema extends BaseFieldSchema { 15 | type: "single"; 16 | } 17 | 18 | export interface MultipleFieldSchema extends BaseFieldSchema { 19 | type: "multiple"; 20 | maxCount?: number; 21 | } 22 | 23 | export type FieldSchema = SingleFieldSchema | MultipleFieldSchema; 24 | 25 | export type FieldValue = string | File; 26 | 27 | const isFile = (value: unknown): value is File => { 28 | return value instanceof File; 29 | }; 30 | 31 | export const FILES_KEY = "files"; 32 | 33 | export class HonoStorage { 34 | options: HonoStorageOptions; 35 | 36 | constructor(options?: HonoStorageOptions) { 37 | this.options = options ?? {}; 38 | } 39 | 40 | private handleSingleStorage = async ( 41 | c: Context, 42 | file: File, 43 | fieldName: string, 44 | ): Promise => { 45 | if (this.options.storage) { 46 | await this.options.storage(c, [ 47 | new HonoStorageFile(file, { 48 | name: fieldName, 49 | type: "single", 50 | }), 51 | ]); 52 | } 53 | }; 54 | 55 | private handleMultipleStorage = async ( 56 | c: Context, 57 | files: File[], 58 | fieldName: string, 59 | ): Promise => { 60 | if (this.options.storage) { 61 | await this.options.storage( 62 | c, 63 | files.map( 64 | (file) => 65 | new HonoStorageFile(file, { 66 | name: fieldName, 67 | type: "multiple", 68 | }), 69 | ), 70 | ); 71 | } 72 | }; 73 | 74 | single = ( 75 | name: T, 76 | _options?: Omit, 77 | ): MiddlewareHandler<{ 78 | Variables: { 79 | [FILES_KEY]: { 80 | [key in T]?: FieldValue; 81 | }; 82 | }; 83 | }> => { 84 | return async (c, next) => { 85 | const formData = await c.req.parseBody({ all: true }); 86 | const value = formData[name]; 87 | if (isFile(value)) { 88 | await this.handleSingleStorage(c, value, name); 89 | } 90 | 91 | c.set(FILES_KEY, { 92 | ...c.get(FILES_KEY), 93 | [name]: value, 94 | }); 95 | 96 | await next(); 97 | }; 98 | }; 99 | 100 | multiple = ( 101 | name: T, 102 | options?: Omit, 103 | ): MiddlewareHandler<{ 104 | Variables: { 105 | [FILES_KEY]: { 106 | [key in T]: FieldValue[]; 107 | }; 108 | }; 109 | }> => { 110 | return async (c, next) => { 111 | const formData = await c.req.parseBody({ all: true }); 112 | const value = formData[name]; 113 | const filedFiles: File[] = []; 114 | 115 | if (Array.isArray(value)) { 116 | filedFiles.push(...value.filter(isFile)); 117 | } else if (isFile(value)) { 118 | filedFiles.push(value); 119 | } 120 | 121 | if (options?.maxCount && filedFiles.length > options.maxCount) { 122 | throw new Error("Too many files"); 123 | } 124 | 125 | await this.handleMultipleStorage(c, filedFiles, name); 126 | 127 | c.set(FILES_KEY, { 128 | ...c.get(FILES_KEY), 129 | [name]: value, 130 | }); 131 | 132 | await next(); 133 | }; 134 | }; 135 | 136 | fields = >( 137 | schema: T, 138 | ): MiddlewareHandler<{ 139 | Variables: { 140 | [FILES_KEY]: { 141 | [key in keyof T]: T[key]["type"] extends "single" 142 | ? FieldValue | undefined 143 | : FieldValue[]; 144 | }; 145 | }; 146 | }> => { 147 | return async (c, next) => { 148 | const formData = await c.req.parseBody({ all: true }); 149 | const uploader: Promise[] = []; 150 | const files: Record = {}; 151 | 152 | for (const name in schema) { 153 | const value = formData[name]; 154 | const field = schema[name]; 155 | 156 | if (field.type === "multiple") { 157 | const filedFiles: File[] = []; 158 | if (Array.isArray(value)) { 159 | filedFiles.push(...value.filter(isFile)); 160 | } else if (isFile(value)) { 161 | filedFiles.push(value); 162 | } 163 | 164 | if (field.maxCount && filedFiles.length > field.maxCount) { 165 | throw Errors.TooManyFiles; 166 | } 167 | uploader.push(this.handleMultipleStorage(c, filedFiles, name)); 168 | files[name] = [value].flat(); 169 | continue; 170 | } 171 | 172 | if (field.type === "single") { 173 | if (isFile(value)) { 174 | uploader.push(this.handleSingleStorage(c, value, name)); 175 | } 176 | files[name] = value; 177 | continue; 178 | } 179 | } 180 | 181 | await Promise.all(uploader); 182 | 183 | c.set(FILES_KEY, { 184 | ...c.get(FILES_KEY), 185 | ...files, 186 | }); 187 | 188 | await next(); 189 | }; 190 | }; 191 | } 192 | -------------------------------------------------------------------------------- /packages/core/tests/file.test.ts: -------------------------------------------------------------------------------- 1 | import { HonoStorageFile } from "../src/file"; 2 | 3 | describe("HonoStorageFile", () => { 4 | it("should be able to create a new instance", () => { 5 | const file = new HonoStorageFile(new File([], "sample1.txt"), { 6 | name: "file", 7 | type: "single", 8 | }); 9 | expect(file).toBeInstanceOf(HonoStorageFile); 10 | }); 11 | 12 | describe("originalname", () => { 13 | it("should work with file without extension", () => { 14 | const file = new HonoStorageFile(new File([], "sample1"), { 15 | name: "file", 16 | type: "single", 17 | }); 18 | expect(file.originalname).toBe("sample1"); 19 | }); 20 | 21 | it("should work with file with extension", () => { 22 | const file = new HonoStorageFile(new File([], "sample1.txt"), { 23 | name: "file", 24 | type: "single", 25 | }); 26 | expect(file.originalname).toBe("sample1"); 27 | }); 28 | 29 | it("should work with file with multiple dots", () => { 30 | const file = new HonoStorageFile(new File([], "sample1.txt.zip"), { 31 | name: "file", 32 | type: "single", 33 | }); 34 | expect(file.originalname).toBe("sample1.txt"); 35 | }); 36 | }); 37 | 38 | describe("extension", () => { 39 | it("should work with file without extension", () => { 40 | const file = new HonoStorageFile(new File([], "sample1"), { 41 | name: "file", 42 | type: "single", 43 | }); 44 | expect(file.extension).toBe(""); 45 | }); 46 | 47 | it("should work with file with extension", () => { 48 | const file = new HonoStorageFile(new File([], "sample1.txt"), { 49 | name: "file", 50 | type: "single", 51 | }); 52 | expect(file.extension).toBe("txt"); 53 | }); 54 | 55 | it("should work with file with multiple dots", () => { 56 | const file = new HonoStorageFile(new File([], "sample1.txt.zip"), { 57 | name: "file", 58 | type: "single", 59 | }); 60 | expect(file.extension).toBe("zip"); 61 | }); 62 | }); 63 | 64 | it("should inherit type from original File", () => { 65 | const file = new HonoStorageFile( 66 | new File([], "sample1.txt", { 67 | type: "text/plain", 68 | }), 69 | { 70 | name: "file", 71 | type: "single", 72 | }, 73 | ); 74 | expect(file.type).toBe("text/plain"); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/core/tests/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | 3 | import { HonoStorage, FieldValue, Errors } from "../src"; 4 | 5 | describe("HonoStorage", () => { 6 | it("should be able to create a new instance", () => { 7 | const storage = new HonoStorage(); 8 | expect(storage).toBeInstanceOf(HonoStorage); 9 | }); 10 | 11 | const storageHandler = vi.fn(); 12 | const storage = new HonoStorage({ 13 | storage: (_, files) => { 14 | files.forEach(() => { 15 | storageHandler(); 16 | }); 17 | }, 18 | }); 19 | 20 | let app = new Hono(); 21 | 22 | beforeEach(() => { 23 | app = new Hono(); 24 | }); 25 | 26 | afterEach(() => { 27 | storageHandler.mockClear(); 28 | }); 29 | 30 | describe("single", () => { 31 | it("can be used as a text parser middleware", async () => { 32 | let actualFieldValue: FieldValue | undefined = undefined; 33 | app.post("/upload", storage.single("text"), (c) => { 34 | expectTypeOf(c.var.files.text).toEqualTypeOf(); 35 | actualFieldValue = c.var.files.text; 36 | return c.text("OK"); 37 | }); 38 | 39 | const formData = new FormData(); 40 | 41 | formData.append("text", "File"); 42 | 43 | const res = await app.request("http://localhost/upload", { 44 | method: "POST", 45 | body: formData, 46 | }); 47 | 48 | expect(res.status).toBe(200); 49 | expect(storageHandler).not.toBeCalled(); 50 | expect(actualFieldValue).toBe("File"); 51 | }); 52 | 53 | it("can be used as a text file upload middleware", async () => { 54 | let actualFieldValue: FieldValue | undefined = undefined; 55 | app.post("/upload", storage.single("file"), (c) => { 56 | expectTypeOf(c.var.files.file).toEqualTypeOf(); 57 | actualFieldValue = c.var.files.file; 58 | return c.text("OK"); 59 | }); 60 | 61 | const formData = new FormData(); 62 | 63 | const file = new File(["File 1"], "sample1.txt"); 64 | formData.append("file", file); 65 | 66 | const res = await app.request("http://localhost/upload", { 67 | method: "POST", 68 | body: formData, 69 | }); 70 | 71 | expect(res.status).toBe(200); 72 | expect(storageHandler).toBeCalledTimes(1); 73 | expect(actualFieldValue).toBeInstanceOf(File); 74 | }); 75 | 76 | it("should through if no form data is provided", async () => { 77 | let actualFieldValue: FieldValue | undefined = undefined; 78 | app.post("/upload", storage.single("text"), (c) => { 79 | actualFieldValue = c.var.files.text; 80 | return c.text("OK"); 81 | }); 82 | 83 | const formData = new FormData(); 84 | 85 | const res = await app.request("http://localhost/upload", { 86 | method: "POST", 87 | body: formData, 88 | }); 89 | 90 | expect(res.status).toBe(200); 91 | expect(storageHandler).not.toBeCalled(); 92 | expect(actualFieldValue).toBeUndefined(); 93 | }); 94 | 95 | it("should work with single chain", async () => { 96 | let actualFieldValue1: FieldValue | undefined = undefined; 97 | let actualFieldValue2: FieldValue | undefined = undefined; 98 | app.post( 99 | "/upload", 100 | storage.single("file1"), 101 | storage.single("file2"), 102 | (c) => { 103 | expectTypeOf(c.var.files.file1).toEqualTypeOf< 104 | FieldValue | undefined 105 | >(); 106 | expectTypeOf(c.var.files.file2).toEqualTypeOf< 107 | FieldValue | undefined 108 | >(); 109 | actualFieldValue1 = c.var.files.file1; 110 | actualFieldValue2 = c.var.files.file2; 111 | return c.text("OK"); 112 | }, 113 | ); 114 | 115 | const formData = new FormData(); 116 | 117 | const file1 = new File(["File 1"], "sample1.txt"); 118 | const file2 = "File 2 (string)"; 119 | formData.append("file1", file1); 120 | formData.append("file2", file2); 121 | 122 | const res = await app.request("http://localhost/upload", { 123 | method: "POST", 124 | body: formData, 125 | }); 126 | 127 | expect(res.status).toBe(200); 128 | expect(storageHandler).toBeCalledTimes(1); // 2 file, but 1 is string 129 | expect(actualFieldValue1).toBeInstanceOf(File); 130 | expect(actualFieldValue2).toBe(file2); 131 | }); 132 | }); 133 | 134 | describe("multiple", () => { 135 | describe("should work depending on the number of files", () => { 136 | let formData: FormData; 137 | 138 | beforeEach(() => { 139 | app.post("/upload", storage.multiple("file"), (c) => c.text("OK")); 140 | formData = new FormData(); 141 | }); 142 | 143 | it("should work if field items count is 0", async () => { 144 | await app.request("http://localhost/upload", { 145 | method: "POST", 146 | body: formData, 147 | }); 148 | 149 | expect(storageHandler).toBeCalledTimes(0); 150 | }); 151 | 152 | it("should work if field items count is 1", async () => { 153 | formData.append("file", new File([`File 1`], "sample1.txt")); 154 | 155 | await app.request("http://localhost/upload", { 156 | method: "POST", 157 | body: formData, 158 | }); 159 | 160 | expect(storageHandler).toBeCalledTimes(1); 161 | }); 162 | 163 | it("should work if field items count is 10", async () => { 164 | for (let i = 0; i < 10; i++) { 165 | formData.append("file", new File([`File ${i}`], `sample${i}.txt`)); 166 | } 167 | 168 | await app.request("http://localhost/upload", { 169 | method: "POST", 170 | body: formData, 171 | }); 172 | 173 | expect(storageHandler).toBeCalledTimes(10); 174 | }); 175 | }); 176 | 177 | describe("should work depending on the number of files with maxCount", () => { 178 | const onErr = vi.fn(); 179 | 180 | beforeEach(() => { 181 | onErr.mockClear(); 182 | app.post( 183 | "/upload", 184 | storage.multiple("file", { 185 | maxCount: 3, 186 | }), 187 | (c) => c.text("OK"), 188 | ); 189 | app.onError((err, c) => { 190 | onErr(err); 191 | return c.text(err.message); 192 | }); 193 | }); 194 | 195 | it("should work if maxCount is set and the number of files is less than maxCount", async () => { 196 | const formData = new FormData(); 197 | for (let i = 0; i < 2; i++) { 198 | formData.append("file", new File([`File ${i}`], `sample${i}.txt`)); 199 | } 200 | await app.request("http://localhost/upload", { 201 | method: "POST", 202 | body: formData, 203 | }); 204 | 205 | expect(storageHandler).toBeCalledTimes(2); 206 | expect(onErr).toBeCalledTimes(0); 207 | }); 208 | 209 | it("should work if maxCount is set and the number of files is greater than maxCount", async () => { 210 | const formData = new FormData(); 211 | for (let i = 0; i < 4; i++) { 212 | formData.append("file", new File([`File ${i}`], `sample${i}.txt`)); 213 | } 214 | 215 | await app.request("http://localhost/upload", { 216 | method: "POST", 217 | body: formData, 218 | }); 219 | 220 | expect(storageHandler).toBeCalledTimes(0); 221 | expect(onErr).toBeCalledTimes(1); 222 | expect(onErr.mock.calls[0][0].message).toBe("Too many files"); 223 | }); 224 | }); 225 | 226 | it("can get the multipart/form-data from the context", async () => { 227 | app.post("/upload", storage.multiple("file"), (c) => { 228 | expectTypeOf(c.var.files.file).toEqualTypeOf; 229 | expect(c.var.files.file).toHaveLength(10); 230 | return c.text("OK"); 231 | }); 232 | 233 | const formData = new FormData(); 234 | for (let i = 0; i < 5; i++) { 235 | formData.append("file", new File([`File ${i}`], `sample${i}.txt`)); 236 | formData.append("file", "File " + i + " (string)"); 237 | } 238 | 239 | const res = await app.request("http://localhost/upload", { 240 | method: "POST", 241 | body: formData, 242 | }); 243 | 244 | expect(res.status).toBe(200); 245 | expect(storageHandler).toBeCalledTimes(5); // 10 files, but 5 are strings 246 | }); 247 | }); 248 | 249 | describe("fields", () => { 250 | it("should work with a single field", async () => { 251 | let actualFieldValue: FieldValue | undefined = undefined; 252 | app.post("/upload", storage.fields({ file: { type: "single" } }), (c) => { 253 | expectTypeOf(c.var.files.file).toEqualTypeOf(); 254 | actualFieldValue = c.var.files.file; 255 | return c.text("OK"); 256 | }); 257 | 258 | const formData = new FormData(); 259 | formData.append("file", new File([`File 1`], "sample1.txt")); 260 | 261 | await app.request("http://localhost/upload", { 262 | method: "POST", 263 | body: formData, 264 | }); 265 | 266 | expect(storageHandler).toBeCalledTimes(1); 267 | expect(actualFieldValue).toBeInstanceOf(File); 268 | }); 269 | 270 | it("should work with a multiple field", async () => { 271 | app.post("/upload", storage.fields({ file: { type: "multiple" } }), (c) => 272 | c.text("OK"), 273 | ); 274 | 275 | const formData = new FormData(); 276 | formData.append("file", new File([`File 1`], "sample1.txt")); 277 | 278 | await app.request("http://localhost/upload", { 279 | method: "POST", 280 | body: formData, 281 | }); 282 | 283 | expect(storageHandler).toBeCalledTimes(1); 284 | }); 285 | 286 | it("should work with a multiple field with maxCount", async () => { 287 | app.post( 288 | "/upload", 289 | storage.fields({ file: { type: "multiple", maxCount: 3 } }), 290 | (c) => c.text("OK"), 291 | ); 292 | 293 | const formData = new FormData(); 294 | for (let i = 0; i < 2; i++) { 295 | formData.append("file", new File([`File ${i}`], "sample1.txt")); 296 | } 297 | 298 | await app.request("http://localhost/upload", { 299 | method: "POST", 300 | body: formData, 301 | }); 302 | 303 | expect(storageHandler).toBeCalledTimes(2); 304 | }); 305 | 306 | it("should work with a multiple field with maxCount and throw an error", async () => { 307 | const onErr = vi.fn(); 308 | 309 | app.post( 310 | "/upload", 311 | storage.fields({ file: { type: "multiple", maxCount: 3 } }), 312 | (c) => c.text("OK"), 313 | ); 314 | app.onError((err, c) => { 315 | onErr(err); 316 | return c.text("OK"); 317 | }); 318 | 319 | const formData = new FormData(); 320 | for (let i = 0; i < 5; i++) { 321 | formData.append("file", new File([`File ${i}`], "sample1.txt")); 322 | } 323 | 324 | await app.request("http://localhost/upload", { 325 | method: "POST", 326 | body: formData, 327 | }); 328 | 329 | expect(storageHandler).toBeCalledTimes(0); 330 | expect(onErr).toBeCalledTimes(1); 331 | expect(onErr.mock.calls[0][0].message).toBe(Errors.TooManyFiles.message); 332 | }); 333 | 334 | it("can get the multipart/form-data from the context", async () => { 335 | let actualFieldValue1: FieldValue | undefined = undefined; 336 | let actualFieldValue2: FieldValue[] | undefined = undefined; 337 | app.post( 338 | "/upload", 339 | storage.fields({ 340 | file1: { type: "single" }, 341 | file2: { type: "multiple" }, 342 | }), 343 | (c) => { 344 | expectTypeOf(c.var.files.file1).toEqualTypeOf< 345 | FieldValue | undefined 346 | >(); 347 | expectTypeOf(c.var.files.file2).toEqualTypeOf(); 348 | actualFieldValue1 = c.var.files.file1; 349 | actualFieldValue2 = c.var.files.file2; 350 | return c.text("OK"); 351 | }, 352 | ); 353 | 354 | const formData = new FormData(); 355 | formData.append("file1", new File([`File 1`], "sample1.txt")); 356 | formData.append("file2", new File([`File 2`], "sample2.txt")); 357 | formData.append("file2", new File([`File 3`], "sample3.txt")); 358 | 359 | await app.request("http://localhost/upload", { 360 | method: "POST", 361 | body: formData, 362 | }); 363 | 364 | expect(storageHandler).toBeCalledTimes(3); 365 | expect(actualFieldValue1).toBeInstanceOf(File); 366 | expect(actualFieldValue2).toHaveLength(2); 367 | expect((actualFieldValue2 as unknown as FieldValue[])[0]).toBeInstanceOf( 368 | File, 369 | ); 370 | }); 371 | }); 372 | }); 373 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./src", 6 | "types": ["vitest/globals"] 7 | }, 8 | "include": ["./src/**/*.ts", "./tests/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/memory/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/memory 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`1649e17`](https://github.com/sor4chi/hono-storage/commit/1649e172335fd7780f04412f72769ab7d991a790)]: 8 | - @hono-storage/core@0.0.14 9 | 10 | ## 0.0.12 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`474a669`](https://github.com/sor4chi/hono-storage/commit/474a669a8f43156aafa58173390504d355ff1b7f)]: 15 | - @hono-storage/core@0.0.13 16 | 17 | ## 0.0.11 18 | 19 | ### Patch Changes 20 | 21 | - [#52](https://github.com/sor4chi/hono-storage/pull/52) [`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b) Thanks [@sor4chi](https://github.com/sor4chi)! - Add require exports field for cjs usecase 22 | 23 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 24 | - @hono-storage/core@0.0.12 25 | 26 | ## 0.0.10 27 | 28 | ### Patch Changes 29 | 30 | - [#51](https://github.com/sor4chi/hono-storage/pull/51) [`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917) Thanks [@sor4chi](https://github.com/sor4chi)! - Move Hono to peerDependencies. Hono Storage now has compatibility with Hono v3.8 or later. 31 | 32 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 33 | - @hono-storage/core@0.0.11 34 | 35 | ## 0.0.9 36 | 37 | ### Patch Changes 38 | 39 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 40 | 41 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 42 | - @hono-storage/core@0.0.10 43 | 44 | ## 0.0.8 45 | 46 | ### Patch Changes 47 | 48 | - [`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3) Thanks [@sor4chi](https://github.com/sor4chi)! - fix package.json 49 | 50 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 51 | - @hono-storage/core@0.0.9 52 | 53 | ## 0.0.7 54 | 55 | ### Patch Changes 56 | 57 | - Updated dependencies [[`0fffc7f`](https://github.com/sor4chi/hono-storage/commit/0fffc7f76152df882b15398014ca8aa331a6ff12)]: 58 | - @hono-storage/core@0.0.8 59 | 60 | ## 0.0.6 61 | 62 | ### Patch Changes 63 | 64 | - Updated dependencies [[`17d6090`](https://github.com/sor4chi/hono-storage/commit/17d609093ade861c93eaac5418ca0a7debb7bebb), [`0d257d4`](https://github.com/sor4chi/hono-storage/commit/0d257d42f158bc4485e907d601a6541d0f25a923)]: 65 | - @hono-storage/core@0.0.7 66 | 67 | ## 0.0.5 68 | 69 | ### Patch Changes 70 | 71 | - Updated dependencies [[`acf1f0d`](https://github.com/sor4chi/hono-storage/commit/acf1f0de6d1c88224182ead9aff3578c5c8842d4), [`6da696f`](https://github.com/sor4chi/hono-storage/commit/6da696f952a6bfeac95725bd077deebba9da8591)]: 72 | - @hono-storage/core@0.0.6 73 | 74 | ## 0.0.4 75 | 76 | ### Patch Changes 77 | 78 | - Updated dependencies [[`51fa375`](https://github.com/sor4chi/hono-storage/commit/51fa3752a49ddb7403edb57b0f1a1feaf154978b)]: 79 | - @hono-storage/core@0.0.5 80 | 81 | ## 0.0.3 82 | 83 | ### Patch Changes 84 | 85 | - Updated dependencies [[`07d2d99`](https://github.com/sor4chi/hono-storage/commit/07d2d99cdf20a1694cc03c965da773754ad6fa61)]: 86 | - @hono-storage/core@0.0.4 87 | 88 | ## 0.0.2 89 | 90 | ### Patch Changes 91 | 92 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 93 | 94 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 95 | - @hono-storage/core@0.0.3 96 | 97 | ## 0.0.1 98 | 99 | ### Patch Changes 100 | 101 | - [#9](https://github.com/sor4chi/hono-storage/pull/9) [`845d497`](https://github.com/sor4chi/hono-storage/commit/845d497f8f0c604dd81839150cdc7c8de5104c66) Thanks [@sor4chi](https://github.com/sor4chi)! - Introduced a new storage called `MemoryStorage`! 102 | 103 | This storage is useful for testing and prototyping, but should not be used in production. 104 | 105 | ```ts 106 | import { serve } from "@hono/node-server"; 107 | import { HonoMemoryStorage } from "@hono-storage/memory"; 108 | import { Hono } from "hono"; 109 | 110 | const app = new Hono(); 111 | const storage = new HonoMemoryStorage({ 112 | key: (c, file) => `${file.originalname}-${new Date()}`, 113 | }); 114 | 115 | app.post("/", storage.single("file"), (c) => c.text("OK")); 116 | app.get("/list", (c) => c.json(storage.buffer.forEach((file) => file.name))); 117 | 118 | serve(app); 119 | ``` 120 | -------------------------------------------------------------------------------- /packages/memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/memory", 3 | "version": "0.0.13", 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint --fix --ext .ts,.tsx src", 10 | "lint:check": "eslint --ext .ts,.tsx src", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\"", 12 | "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", 13 | "build": "tsup ./src/index.ts --format esm,cjs --dts", 14 | "test": "vitest run" 15 | }, 16 | "keywords": [ 17 | "hono" 18 | ], 19 | "files": [ 20 | "dist" 21 | ], 22 | "exports": { 23 | ".": { 24 | "types": "./dist/index.d.ts", 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "author": "sor4chi", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@hono-storage/core": "workspace:*" 33 | }, 34 | "devDependencies": { 35 | "vitest": "^0.34.5", 36 | "hono": "^4.3.2" 37 | }, 38 | "peerDependencies": { 39 | "hono": ">=3.8" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/memory/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HonoStorage, HonoStorageFile } from "@hono-storage/core"; 2 | 3 | import type { Context } from "hono"; 4 | 5 | type HMSFunction = (c: Context, file: HonoStorageFile) => string; 6 | 7 | export type HonoMemoryStorageOptions = { 8 | key?: HMSFunction; 9 | }; 10 | 11 | export class HonoMemoryStorage extends HonoStorage { 12 | buffer: Map; 13 | key: HMSFunction; 14 | 15 | constructor(options: HonoMemoryStorageOptions = {}) { 16 | super({ 17 | storage: async (c, files) => { 18 | files.forEach((file) => { 19 | this.buffer.set(this.key(c, file), file); 20 | }); 21 | }, 22 | }); 23 | 24 | this.key = options.key ?? ((_, file) => file.name); 25 | this.buffer = new Map(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/memory/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | 3 | import { HonoMemoryStorage } from "../src"; 4 | 5 | describe("HonoMemoryStorage", () => { 6 | it("should be able to create a new instance", () => { 7 | const storage = new HonoMemoryStorage(); 8 | expect(storage).toBeInstanceOf(HonoMemoryStorage); 9 | }); 10 | 11 | it("should work with default option", async () => { 12 | const storage = new HonoMemoryStorage(); 13 | const app = new Hono(); 14 | app.post( 15 | "/upload", 16 | storage.fields({ 17 | file: { type: "multiple", maxCount: 1 }, 18 | file2: { type: "multiple", maxCount: 2 }, 19 | }), 20 | (c) => c.text("Hello World"), 21 | ); 22 | 23 | const formData = new FormData(); 24 | 25 | const file1 = new File(["Hello World 1"], "sample1.txt", { 26 | type: "text/plain", 27 | }); 28 | const file2 = new File(["Hello World 2"], "sample2.txt", { 29 | type: "text/plain", 30 | }); 31 | const file3 = new File(["Hello World 3"], "sample1.txt", { 32 | type: "text/plain", 33 | }); 34 | formData.append("file", file1); 35 | formData.append("file2", file2); 36 | formData.append("file2", file3); 37 | 38 | const res = await app.request("http://localhost/upload", { 39 | method: "POST", 40 | body: formData, 41 | }); 42 | 43 | expect(res.status).toBe(200); 44 | expect(await res.text()).toBe("Hello World"); 45 | expect(storage.buffer).toHaveLength(2); 46 | expect(storage.buffer.get("sample1.txt")).not.toBeUndefined(); 47 | expect(storage.buffer.get("sample2.txt")).not.toBeUndefined(); 48 | expect(await storage.buffer.get("sample1.txt")?.text()).toBe( 49 | "Hello World 3", 50 | ); 51 | expect(await storage.buffer.get("sample2.txt")?.text()).toBe( 52 | "Hello World 2", 53 | ); 54 | }); 55 | 56 | it("should work with custom key option", async () => { 57 | const storage = new HonoMemoryStorage({ 58 | key: (c, file) => `${c.req.query("store")}/${file.name}`, 59 | }); 60 | const app = new Hono(); 61 | app.post( 62 | "/upload", 63 | storage.fields({ 64 | file: { type: "multiple", maxCount: 1 }, 65 | file2: { type: "multiple", maxCount: 2 }, 66 | }), 67 | (c) => c.text("Hello World"), 68 | ); 69 | 70 | const formData = new FormData(); 71 | 72 | const file1 = new File(["Hello World 1"], "sample1.txt", { 73 | type: "text/plain", 74 | }); 75 | const file2 = new File(["Hello World 2"], "sample2.txt", { 76 | type: "text/plain", 77 | }); 78 | const file3 = new File(["Hello World 3"], "sample1.txt", { 79 | type: "text/plain", 80 | }); 81 | formData.append("file", file1); 82 | formData.append("file2", file2); 83 | formData.append("file2", file3); 84 | 85 | const res = await app.request("http://localhost/upload?store=1", { 86 | method: "POST", 87 | body: formData, 88 | }); 89 | 90 | expect(res.status).toBe(200); 91 | expect(await res.text()).toBe("Hello World"); 92 | expect(storage.buffer).toHaveLength(2); 93 | expect(storage.buffer.get("1/sample1.txt")).not.toBeUndefined(); 94 | expect(storage.buffer.get("1/sample2.txt")).not.toBeUndefined(); 95 | expect(await storage.buffer.get("1/sample1.txt")?.text()).toBe( 96 | "Hello World 3", 97 | ); 98 | expect(await storage.buffer.get("1/sample2.txt")?.text()).toBe( 99 | "Hello World 2", 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/memory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./src", 6 | "types": ["vitest/globals"] 7 | }, 8 | "include": ["./src/**/*.ts", "./tests/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/memory/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/node-disk/.gitignore: -------------------------------------------------------------------------------- 1 | tests/tmp 2 | -------------------------------------------------------------------------------- /packages/node-disk/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/node-disk 2 | 3 | ## 0.0.16 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`1649e17`](https://github.com/sor4chi/hono-storage/commit/1649e172335fd7780f04412f72769ab7d991a790)]: 8 | - @hono-storage/core@0.0.14 9 | 10 | ## 0.0.15 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`474a669`](https://github.com/sor4chi/hono-storage/commit/474a669a8f43156aafa58173390504d355ff1b7f)]: 15 | - @hono-storage/core@0.0.13 16 | 17 | ## 0.0.14 18 | 19 | ### Patch Changes 20 | 21 | - [#55](https://github.com/sor4chi/hono-storage/pull/55) [`e5ed56e`](https://github.com/sor4chi/hono-storage/commit/e5ed56e787c81986102fd59d1d5ad951fe0ac64b) Thanks [@sor4chi](https://github.com/sor4chi)! - enable use of dest option as async function 22 | 23 | ## 0.0.13 24 | 25 | ### Patch Changes 26 | 27 | - [#52](https://github.com/sor4chi/hono-storage/pull/52) [`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b) Thanks [@sor4chi](https://github.com/sor4chi)! - Add require exports field for cjs usecase 28 | 29 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 30 | - @hono-storage/core@0.0.12 31 | 32 | ## 0.0.12 33 | 34 | ### Patch Changes 35 | 36 | - [#51](https://github.com/sor4chi/hono-storage/pull/51) [`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917) Thanks [@sor4chi](https://github.com/sor4chi)! - Move Hono to peerDependencies. Hono Storage now has compatibility with Hono v3.8 or later. 37 | 38 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 39 | - @hono-storage/core@0.0.11 40 | 41 | ## 0.0.11 42 | 43 | ### Patch Changes 44 | 45 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 46 | 47 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 48 | - @hono-storage/core@0.0.10 49 | 50 | ## 0.0.10 51 | 52 | ### Patch Changes 53 | 54 | - [`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3) Thanks [@sor4chi](https://github.com/sor4chi)! - fix package.json 55 | 56 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 57 | - @hono-storage/core@0.0.9 58 | 59 | ## 0.0.9 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [[`0fffc7f`](https://github.com/sor4chi/hono-storage/commit/0fffc7f76152df882b15398014ca8aa331a6ff12)]: 64 | - @hono-storage/core@0.0.8 65 | 66 | ## 0.0.8 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [[`17d6090`](https://github.com/sor4chi/hono-storage/commit/17d609093ade861c93eaac5418ca0a7debb7bebb), [`0d257d4`](https://github.com/sor4chi/hono-storage/commit/0d257d42f158bc4485e907d601a6541d0f25a923)]: 71 | - @hono-storage/core@0.0.7 72 | 73 | ## 0.0.7 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies [[`acf1f0d`](https://github.com/sor4chi/hono-storage/commit/acf1f0de6d1c88224182ead9aff3578c5c8842d4), [`6da696f`](https://github.com/sor4chi/hono-storage/commit/6da696f952a6bfeac95725bd077deebba9da8591)]: 78 | - @hono-storage/core@0.0.6 79 | 80 | ## 0.0.6 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [[`51fa375`](https://github.com/sor4chi/hono-storage/commit/51fa3752a49ddb7403edb57b0f1a1feaf154978b)]: 85 | - @hono-storage/core@0.0.5 86 | 87 | ## 0.0.5 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`07d2d99`](https://github.com/sor4chi/hono-storage/commit/07d2d99cdf20a1694cc03c965da773754ad6fa61)]: 92 | - @hono-storage/core@0.0.4 93 | 94 | ## 0.0.4 95 | 96 | ### Patch Changes 97 | 98 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 99 | 100 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 101 | - @hono-storage/core@0.0.3 102 | 103 | ## 0.0.3 104 | 105 | ### Patch Changes 106 | 107 | - Updated dependencies [[`ea1eb7a`](https://github.com/sor4chi/hono-storage/commit/ea1eb7a533b8ba3d08acc80f92b8153a9048bfc9)]: 108 | - @hono-storage/core@0.0.2 109 | 110 | ## 0.0.2 111 | 112 | ### Patch Changes 113 | 114 | - [#3](https://github.com/sor4chi/hono-storage/pull/3) [`da24913`](https://github.com/sor4chi/hono-storage/commit/da249130275d6a2c2827f17cdd1778bfb2fe34f9) Thanks [@sor4chi](https://github.com/sor4chi)! - Support more dynamic dest path. 115 | You can decide the dest path by the context and file. 116 | 117 | ## Before 118 | 119 | ```ts 120 | const storage = new NodeDiskStorage({ 121 | dest: "/path/to/dest", 122 | }); 123 | ``` 124 | 125 | ## After 126 | 127 | Also support function. 128 | 129 | ```ts 130 | const storage = new NodeDiskStorage({ 131 | dest: (c, file) => { 132 | return "/path/to/dest"; 133 | }, 134 | }); 135 | ``` 136 | 137 | ## 0.0.1 138 | 139 | ### Patch Changes 140 | 141 | - [`472a0a3`](https://github.com/sor4chi/hono-storage/commit/472a0a39cd750b3483d01c5b72bec816c7b8cac9) Thanks [@sor4chi](https://github.com/sor4chi)! - This is Hono Storage for Node.js, a simple and easy to use file storage library. 142 | 143 | ```bash 144 | npm install @hono-storage/node-disk 145 | ``` 146 | 147 | ```ts 148 | import { serve } from "@hono/node-server"; 149 | import { HonoStorage } from "@hono-storage/node-disk"; 150 | import { Hono } from "hono"; 151 | 152 | const app = new Hono(); 153 | const storage = new HonoStorage({ 154 | dest: "./uploads", 155 | filename: (c, file) => 156 | `${file.originalname}-${Date.now()}.${file.extension}`, 157 | }); 158 | 159 | app.post("/upload", storage.single("image"), (c) => c.text("OK")); 160 | 161 | serve(app); 162 | ``` 163 | 164 | - Updated dependencies [[`a7ade7f`](https://github.com/sor4chi/hono-storage/commit/a7ade7f3bb67cbf3b70efbdf91e9260043413f16)]: 165 | - @hono-storage/core@0.0.1 166 | -------------------------------------------------------------------------------- /packages/node-disk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/node-disk", 3 | "version": "0.0.16", 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint --fix --ext .ts,.tsx src", 10 | "lint:check": "eslint --ext .ts,.tsx src", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\"", 12 | "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", 13 | "test": "vitest run", 14 | "build": "tsup ./src/index.ts --format esm,cjs --dts" 15 | }, 16 | "keywords": [ 17 | "hono" 18 | ], 19 | "files": [ 20 | "dist" 21 | ], 22 | "exports": { 23 | ".": { 24 | "types": "./dist/index.d.ts", 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "author": "sor4chi", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@hono-storage/core": "workspace:*", 33 | "@web-std/file": "^3.0.3" 34 | }, 35 | "devDependencies": { 36 | "@hono/node-server": "^1.2.0", 37 | "@types/node": "^18.7.6", 38 | "@types/supertest": "^2.0.12", 39 | "supertest": "^6.3.3", 40 | "hono": "^4.3.2" 41 | }, 42 | "peerDependencies": { 43 | "hono": ">=3.8" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/node-disk/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from "fs"; 2 | import { mkdir } from "fs/promises"; 3 | import { join } from "path"; 4 | 5 | import { HonoStorage, HonoStorageFile } from "@hono-storage/core"; 6 | import { File } from "@web-std/file"; 7 | 8 | import type { Context } from "hono"; 9 | 10 | type HDSCustomFunction = ( 11 | c: Context, 12 | file: HonoStorageFile, 13 | ) => Promise | string; 14 | 15 | interface HonoDiskStorageOption { 16 | dest?: string | HDSCustomFunction; 17 | filename?: HDSCustomFunction; 18 | } 19 | 20 | export class HonoDiskStorage extends HonoStorage { 21 | constructor(option: HonoDiskStorageOption = {}) { 22 | const { dest = "/tmp" } = option; 23 | 24 | super({ 25 | storage: async (c, files) => { 26 | await Promise.all( 27 | files.map(async (file) => { 28 | const finalDest = 29 | typeof dest === "function" ? await dest(c, file) : dest; 30 | await mkdir(finalDest, { recursive: true }); 31 | if (option.filename) { 32 | await this.handleDestStorage( 33 | finalDest, 34 | new File([file], await option.filename(c, file)), 35 | ); 36 | } else { 37 | await this.handleDestStorage( 38 | finalDest, 39 | new File([file], file.name), 40 | ); 41 | } 42 | }), 43 | ); 44 | }, 45 | }); 46 | } 47 | 48 | handleDestStorage = async (dest: string, file: File) => { 49 | const writeStream = createWriteStream(join(dest, file.name)); 50 | const reader = file.stream().getReader(); 51 | // eslint-disable-next-line no-constant-condition 52 | while (true) { 53 | const { done, value } = await reader.read(); 54 | if (done) { 55 | break; 56 | } 57 | writeStream.write(value); 58 | } 59 | writeStream.end(); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/node-disk/tests/fixture/sample1.txt: -------------------------------------------------------------------------------- 1 | Hello Hono Storage 1 2 | -------------------------------------------------------------------------------- /packages/node-disk/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { rm } from "fs"; 2 | import { join } from "path"; 3 | 4 | import { createAdaptorServer } from "@hono/node-server"; 5 | import { Hono } from "hono"; 6 | import request from "supertest"; 7 | 8 | import { HonoDiskStorage } from "../src"; 9 | 10 | describe("HonoDiskStorage", () => { 11 | it("should be able to create a new instance", () => { 12 | const storage = new HonoDiskStorage({ 13 | dest: "/tmp", 14 | }); 15 | expect(storage).toBeInstanceOf(HonoDiskStorage); 16 | }); 17 | 18 | describe("dest option", () => { 19 | beforeEach(() => { 20 | rm(join(__dirname, "tmp"), { recursive: true }, () => {}); 21 | }); 22 | 23 | it("should work with string dest", async () => { 24 | const storage = new HonoDiskStorage({ 25 | dest: join(__dirname, "tmp"), 26 | }); 27 | const spyHandleDestStorage = vi.spyOn(storage, "handleDestStorage"); 28 | const app = new Hono(); 29 | app.post("/upload", storage.single("file"), (c) => c.text("Hello World")); 30 | const server = createAdaptorServer(app); 31 | const res = await request(server) 32 | .post("/upload") 33 | .attach("file", join(__dirname, "fixture/sample1.txt")); 34 | expect(res.status).toBe(200); 35 | expect(res.text).toBe("Hello World"); 36 | expect(spyHandleDestStorage).toBeCalledWith( 37 | join(__dirname, "tmp"), 38 | expect.objectContaining({ 39 | name: "sample1.txt", 40 | }), 41 | ); 42 | }); 43 | 44 | it("should work with custom function dest", async () => { 45 | const storage = new HonoDiskStorage({ 46 | dest: (c) => { 47 | if (c.req.query("store")) { 48 | return join(__dirname, `tmp/store${c.req.query("store")}`); 49 | } 50 | return join(__dirname, "tmp"); 51 | }, 52 | }); 53 | const spyHandleDestStorage = vi.spyOn(storage, "handleDestStorage"); 54 | const app = new Hono(); 55 | app.post("/upload", storage.single("file"), (c) => c.text("Hello World")); 56 | const server = createAdaptorServer(app); 57 | const res = await request(server) 58 | .post("/upload?store=1") 59 | .attach("file", join(__dirname, "fixture/sample1.txt")); 60 | expect(res.status).toBe(200); 61 | expect(res.text).toBe("Hello World"); 62 | expect(spyHandleDestStorage).toBeCalledWith( 63 | join(__dirname, "tmp/store1"), 64 | expect.objectContaining({ 65 | name: "sample1.txt", 66 | }), 67 | ); 68 | }); 69 | 70 | it("should work with async custom function dest", async () => { 71 | const storage = new HonoDiskStorage({ 72 | dest: async (c) => { 73 | if (c.req.query("store")) { 74 | return join(__dirname, `tmp/store${c.req.query("store")}`); 75 | } 76 | return join(__dirname, "tmp"); 77 | }, 78 | }); 79 | const spyHandleDestStorage = vi.spyOn(storage, "handleDestStorage"); 80 | const app = new Hono(); 81 | app.post("/upload", storage.single("file"), (c) => c.text("Hello World")); 82 | const server = createAdaptorServer(app); 83 | const res = await request(server) 84 | .post("/upload?store=1") 85 | .attach("file", join(__dirname, "fixture/sample1.txt")); 86 | expect(res.status).toBe(200); 87 | expect(res.text).toBe("Hello World"); 88 | expect(spyHandleDestStorage).toBeCalledWith( 89 | join(__dirname, "tmp/store1"), 90 | expect.objectContaining({ 91 | name: "sample1.txt", 92 | }), 93 | ); 94 | }); 95 | }); 96 | 97 | describe("filename option", () => { 98 | it("can be used as a file upload middleware", async () => { 99 | const PREFIX = "prefix-"; 100 | const storage = new HonoDiskStorage({ 101 | dest: join(__dirname, "tmp"), 102 | filename: (_, file) => 103 | `${PREFIX}${file.originalname}.${file.extension}`, 104 | }); 105 | const spyHandleDestStorage = vi.spyOn(storage, "handleDestStorage"); 106 | const app = new Hono(); 107 | app.post("/upload", storage.single("file"), (c) => c.text("Hello World")); 108 | const server = createAdaptorServer(app); 109 | const res = await request(server) 110 | .post("/upload") 111 | .attach("file", join(__dirname, "fixture/sample1.txt")); 112 | expect(res.status).toBe(200); 113 | expect(res.text).toBe("Hello World"); 114 | expect(spyHandleDestStorage).toBeCalledWith( 115 | join(__dirname, "tmp"), 116 | expect.objectContaining({ 117 | name: `${PREFIX}sample1.txt`, 118 | }), 119 | ); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /packages/node-disk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./src", 6 | "types": ["vitest/globals"] 7 | }, 8 | "include": ["./src/**/*.ts", "./tests/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/node-disk/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | environment: "node", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/s3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono-storage/s3 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`1649e17`](https://github.com/sor4chi/hono-storage/commit/1649e172335fd7780f04412f72769ab7d991a790)]: 8 | - @hono-storage/core@0.0.14 9 | 10 | ## 0.0.12 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`474a669`](https://github.com/sor4chi/hono-storage/commit/474a669a8f43156aafa58173390504d355ff1b7f)]: 15 | - @hono-storage/core@0.0.13 16 | 17 | ## 0.0.11 18 | 19 | ### Patch Changes 20 | 21 | - [#52](https://github.com/sor4chi/hono-storage/pull/52) [`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b) Thanks [@sor4chi](https://github.com/sor4chi)! - Add require exports field for cjs usecase 22 | 23 | - Updated dependencies [[`41803f8`](https://github.com/sor4chi/hono-storage/commit/41803f8dbb3ec30ff03720e510e01563b7153b5b)]: 24 | - @hono-storage/core@0.0.12 25 | 26 | ## 0.0.10 27 | 28 | ### Patch Changes 29 | 30 | - [#51](https://github.com/sor4chi/hono-storage/pull/51) [`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917) Thanks [@sor4chi](https://github.com/sor4chi)! - Move Hono to peerDependencies. Hono Storage now has compatibility with Hono v3.8 or later. 31 | 32 | - Updated dependencies [[`301f6e9`](https://github.com/sor4chi/hono-storage/commit/301f6e9b2e6762b350fc0b3c1316e109fc843917)]: 33 | - @hono-storage/core@0.0.11 34 | 35 | ## 0.0.9 36 | 37 | ### Patch Changes 38 | 39 | - [#47](https://github.com/sor4chi/hono-storage/pull/47) [`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044) Thanks [@sor4chi](https://github.com/sor4chi)! - update Hono's version to v4.3.2 40 | 41 | - Updated dependencies [[`0cc024e`](https://github.com/sor4chi/hono-storage/commit/0cc024eb7dc065bb648f34c52174b0b1baa8d044)]: 42 | - @hono-storage/core@0.0.10 43 | 44 | ## 0.0.8 45 | 46 | ### Patch Changes 47 | 48 | - [`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3) Thanks [@sor4chi](https://github.com/sor4chi)! - fix package.json 49 | 50 | - Updated dependencies [[`628e0dc`](https://github.com/sor4chi/hono-storage/commit/628e0dcd6b48953db1d212e317c1d470499780e3)]: 51 | - @hono-storage/core@0.0.9 52 | 53 | ## 0.0.7 54 | 55 | ### Patch Changes 56 | 57 | - [#38](https://github.com/sor4chi/hono-storage/pull/38) [`ad5332b`](https://github.com/sor4chi/hono-storage/commit/ad5332b6689ad1baeba70406d732d81623779e97) Thanks [@sor4chi](https://github.com/sor4chi)! - feat: support `signedUrl` for s3 storage. 58 | 59 | ```ts 60 | import { S3Client } from "@aws-sdk/client-s3"; 61 | import { HonoS3Storage } from "@hono-storage/s3"; 62 | import { Hono } from "hono"; 63 | 64 | const client = new S3Client({ 65 | region: "us-east-1", 66 | credentials: { 67 | accessKeyId: "...", 68 | secretAccessKey: "...", 69 | }, 70 | }); 71 | 72 | const storage = new HonoS3Storage({ 73 | bucket: "hono-storage", 74 | client, 75 | }); 76 | 77 | const app = new Hono(); 78 | 79 | app.post( 80 | "/", 81 | storage.single("image", { 82 | sign: { 83 | expiresIn: 60, 84 | }, 85 | }), 86 | (c) => { 87 | return c.json({ signedURL: c.var.signedURLs.image }); 88 | }, 89 | ); 90 | ``` 91 | 92 | - Updated dependencies [[`0fffc7f`](https://github.com/sor4chi/hono-storage/commit/0fffc7f76152df882b15398014ca8aa331a6ff12)]: 93 | - @hono-storage/core@0.0.8 94 | 95 | ## 0.0.6 96 | 97 | ### Patch Changes 98 | 99 | - Updated dependencies [[`17d6090`](https://github.com/sor4chi/hono-storage/commit/17d609093ade861c93eaac5418ca0a7debb7bebb), [`0d257d4`](https://github.com/sor4chi/hono-storage/commit/0d257d42f158bc4485e907d601a6541d0f25a923)]: 100 | - @hono-storage/core@0.0.7 101 | 102 | ## 0.0.5 103 | 104 | ### Patch Changes 105 | 106 | - Updated dependencies [[`acf1f0d`](https://github.com/sor4chi/hono-storage/commit/acf1f0de6d1c88224182ead9aff3578c5c8842d4), [`6da696f`](https://github.com/sor4chi/hono-storage/commit/6da696f952a6bfeac95725bd077deebba9da8591)]: 107 | - @hono-storage/core@0.0.6 108 | 109 | ## 0.0.4 110 | 111 | ### Patch Changes 112 | 113 | - Updated dependencies [[`51fa375`](https://github.com/sor4chi/hono-storage/commit/51fa3752a49ddb7403edb57b0f1a1feaf154978b)]: 114 | - @hono-storage/core@0.0.5 115 | 116 | ## 0.0.3 117 | 118 | ### Patch Changes 119 | 120 | - Updated dependencies [[`07d2d99`](https://github.com/sor4chi/hono-storage/commit/07d2d99cdf20a1694cc03c965da773754ad6fa61)]: 121 | - @hono-storage/core@0.0.4 122 | 123 | ## 0.0.2 124 | 125 | ### Patch Changes 126 | 127 | - [`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae) Thanks [@sor4chi](https://github.com/sor4chi)! - Update hono version to 3.8.1 128 | 129 | - Updated dependencies [[`f03e4a4`](https://github.com/sor4chi/hono-storage/commit/f03e4a41d705fa8883cef1dce85784825ea05eae)]: 130 | - @hono-storage/core@0.0.3 131 | 132 | ## 0.0.1 133 | 134 | ### Patch Changes 135 | 136 | - [#12](https://github.com/sor4chi/hono-storage/pull/12) [`ec74110`](https://github.com/sor4chi/hono-storage/commit/ec741102219a960c5a0e8317b0eda3ce4e3f4a14) Thanks [@sor4chi](https://github.com/sor4chi)! - A S3 helper for Hono Storage. Use `@aws-sdk/client-s3` as client. 137 | 138 | ```ts 139 | import { S3Client } from "@aws-sdk/client-s3"; 140 | import { HonoS3Storage } from "@hono-storage/s3"; 141 | import { Hono } from "hono"; 142 | 143 | const app = new Hono(); 144 | 145 | /** For Dynamic Client */ 146 | const client = (accessKeyId: string, secretAccessKey: string) => 147 | new S3Client({ 148 | region: "[your-bucket-region-name]", 149 | credentials: { 150 | accessKeyId, 151 | secretAccessKey, 152 | }, 153 | }); 154 | 155 | const storage = new HonoS3Storage({ 156 | key: (_, file) => `${file.originalname}-${new Date().getTime()}`, 157 | bucket: "[your-bucket-name]", 158 | client: (c) => client(c.env.AWS_ACCESS_KEY_ID, c.env.AWS_SECRET_ACCESS_KEY), 159 | }); 160 | 161 | /** For Static Client */ 162 | const storage = new HonoS3Storage({ 163 | key: (_, file) => `${file.originalname}-${new Date().getTime()}`, 164 | bucket: "[your-bucket-name]", 165 | client: new S3Client({ 166 | region: "[your-bucket-region-name]", 167 | credentials: { 168 | accessKeyId: "[your-access-key-id]", 169 | secretAccessKey: "[your-secret-access-key]", 170 | }, 171 | }), 172 | }); 173 | 174 | app.post("/", storage.single("file"), (c) => c.text("OK")); 175 | 176 | export default app; 177 | ``` 178 | -------------------------------------------------------------------------------- /packages/s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono-storage/s3", 3 | "version": "0.0.13", 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint --fix --ext .ts,.tsx src", 10 | "lint:check": "eslint --ext .ts,.tsx src", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\"", 12 | "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", 13 | "test": "vitest run", 14 | "build": "tsup ./src/index.ts --format esm,cjs --dts" 15 | }, 16 | "keywords": [ 17 | "hono" 18 | ], 19 | "files": [ 20 | "dist" 21 | ], 22 | "exports": { 23 | ".": { 24 | "types": "./dist/index.d.ts", 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "author": "sor4chi", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@aws-sdk/client-s3": "^3.428.0", 33 | "@aws-sdk/s3-request-presigner": "3.428.0", 34 | "@hono-storage/core": "workspace:*" 35 | }, 36 | "devDependencies": { 37 | "@smithy/types": "^2.4.0", 38 | "@web-std/file": "^3.0.3", 39 | "hono": "^4.3.2" 40 | }, 41 | "peerDependencies": { 42 | "hono": ">=3.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/s3/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetObjectCommand, 3 | PutObjectCommand, 4 | S3Client, 5 | } from "@aws-sdk/client-s3"; 6 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 7 | import { HonoStorageFile } from "@hono-storage/core"; 8 | import { RequestPresigningArguments } from "@smithy/types"; 9 | 10 | import { 11 | BaseHonoS3Storage, 12 | HonoS3StorageOptions, 13 | IS3Object, 14 | IS3Sign, 15 | } from "./storage"; 16 | 17 | import type { Context } from "hono"; 18 | 19 | class S3Repository implements IS3Object, IS3Sign { 20 | private client: S3Client; 21 | 22 | constructor(client: S3Client) { 23 | this.client = client; 24 | } 25 | 26 | async put(command: PutObjectCommand): Promise { 27 | await this.client.send(command); 28 | } 29 | 30 | async getSingedURL( 31 | command: GetObjectCommand, 32 | sign: RequestPresigningArguments, 33 | ): Promise { 34 | return await getSignedUrl(this.client, command, sign); 35 | } 36 | } 37 | 38 | export class HonoS3Storage extends BaseHonoS3Storage { 39 | constructor(options: HonoS3StorageOptions) { 40 | const client = options.client; 41 | 42 | if (typeof client !== "function") { 43 | super(options, new S3Repository(client)); 44 | return; 45 | } 46 | 47 | super( 48 | options, 49 | (c: Context, file: HonoStorageFile) => new S3Repository(client(c, file)), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/s3/src/storage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | S3Client, 3 | PutObjectCommand, 4 | PutObjectRequest, 5 | GetObjectCommand, 6 | } from "@aws-sdk/client-s3"; 7 | import { 8 | FILES_KEY, 9 | FieldValue, 10 | HonoStorage, 11 | HonoStorageFile, 12 | MultipleFieldSchema, 13 | SingleFieldSchema, 14 | } from "@hono-storage/core"; 15 | 16 | import type { RequestPresigningArguments } from "@smithy/types"; 17 | import type { Context, MiddlewareHandler } from "hono"; 18 | 19 | type HSSFunction = (c: Context, file: HonoStorageFile) => T; 20 | type UploadCustomParams = Omit< 21 | PutObjectRequest, 22 | "Bucket" | "Key" | "Body" | "ContentType" | "ContentLength" 23 | >; 24 | 25 | export type HonoS3StorageOptions = { 26 | key?: HSSFunction; 27 | bucket: string | HSSFunction; 28 | client: S3Client | HSSFunction; 29 | params?: UploadCustomParams; 30 | }; 31 | 32 | const SIGN_CONFIG_KEY = "hono-storage-s3:sign-config"; 33 | export const SIGNED_URL_KEY = "signedURLs"; 34 | 35 | type SignOption = { 36 | sign?: RequestPresigningArguments; 37 | }; 38 | 39 | export interface IS3Object { 40 | put(command: PutObjectCommand): Promise; 41 | } 42 | 43 | export interface IS3Sign { 44 | getSingedURL( 45 | command: GetObjectCommand, 46 | sign: RequestPresigningArguments, 47 | ): Promise; 48 | } 49 | 50 | export type IS3Repository = IS3Object & IS3Sign; 51 | 52 | export type HSSSingleFieldSchema = SingleFieldSchema & SignOption; 53 | export type HSSMultipleFieldSchema = MultipleFieldSchema & SignOption; 54 | 55 | type HSSFieldSchema = HSSSingleFieldSchema | HSSMultipleFieldSchema; 56 | 57 | export class BaseHonoS3Storage { 58 | private storage: HonoStorage; 59 | private key: HSSFunction; 60 | private bucket: string | HSSFunction; 61 | private params?: UploadCustomParams; 62 | private s3Repository: IS3Repository | HSSFunction; 63 | 64 | constructor( 65 | options: HonoS3StorageOptions, 66 | s3Repository: IS3Repository | HSSFunction, 67 | ) { 68 | this.storage = new HonoStorage({ 69 | storage: async (c, files) => { 70 | await Promise.all( 71 | files.map(async (file) => { 72 | await this.upload(c, file); 73 | }), 74 | ); 75 | }, 76 | }); 77 | 78 | this.key = options.key ?? ((_, file) => file.name); 79 | this.bucket = options.bucket; 80 | this.params = options.params; 81 | this.s3Repository = s3Repository; 82 | } 83 | 84 | async upload( 85 | c: Context<{ 86 | Variables: { 87 | [SIGN_CONFIG_KEY]: Record; 88 | [SIGNED_URL_KEY]: Record; 89 | }; 90 | }>, 91 | file: HonoStorageFile, 92 | ) { 93 | const key = this.key(c, file); 94 | const bucket = 95 | typeof this.bucket === "function" ? this.bucket(c, file) : this.bucket; 96 | 97 | // for nodejs 98 | const isBufferExists = typeof Buffer !== "undefined"; 99 | 100 | const putCommand = new PutObjectCommand({ 101 | Bucket: bucket, 102 | Key: key, 103 | Body: isBufferExists ? Buffer.from(await file.arrayBuffer()) : file, 104 | ContentType: file.type, 105 | ContentLength: file.size, 106 | ...this.params, 107 | }); 108 | 109 | const getCommand = new GetObjectCommand({ 110 | Bucket: bucket, 111 | Key: key, 112 | }); 113 | 114 | const signConfig = c.get(SIGN_CONFIG_KEY) ?? {}; 115 | const sign = signConfig[file.field.name]; 116 | 117 | const s3Repository = 118 | typeof this.s3Repository === "function" 119 | ? this.s3Repository(c, file) 120 | : this.s3Repository; 121 | 122 | await s3Repository.put(putCommand); 123 | 124 | if (sign) { 125 | const signedURL = await s3Repository.getSingedURL(getCommand, sign); 126 | const signedURLs = c.get(SIGNED_URL_KEY) ?? {}; 127 | c.set(SIGNED_URL_KEY, { 128 | ...signedURLs, 129 | [file.field.name]: (() => { 130 | const targetSignField = signedURLs[file.field.name] ?? []; 131 | 132 | if ( 133 | file.field.type === "single" || 134 | typeof targetSignField === "string" 135 | ) { 136 | return signedURL; 137 | } 138 | 139 | return [...targetSignField, signedURL]; 140 | })(), 141 | }); 142 | } 143 | } 144 | 145 | single = ( 146 | name: T, 147 | options?: U, 148 | ): MiddlewareHandler<{ 149 | Variables: { 150 | [FILES_KEY]: { 151 | [key in T]?: FieldValue; 152 | }; 153 | [SIGN_CONFIG_KEY]?: Record; 154 | [SIGNED_URL_KEY]: { 155 | [key in T]?: U extends { sign: RequestPresigningArguments } 156 | ? string 157 | : undefined; 158 | }; 159 | }; 160 | }> => { 161 | return async (c, next) => { 162 | c.set(SIGNED_URL_KEY, { 163 | ...(c.get(SIGNED_URL_KEY) ?? {}), 164 | }); 165 | 166 | if (options?.sign) { 167 | c.set(SIGN_CONFIG_KEY, { 168 | ...(c.get(SIGN_CONFIG_KEY) ?? {}), 169 | [name]: options.sign, 170 | }); 171 | } 172 | 173 | await this.storage.single(name)(c as Context, next); 174 | }; 175 | }; 176 | 177 | multiple = < 178 | T extends string, 179 | U extends Omit & SignOption, 180 | >( 181 | name: T, 182 | options?: U, 183 | ): MiddlewareHandler<{ 184 | Variables: { 185 | [FILES_KEY]: { 186 | [key in T]?: FieldValue; 187 | }; 188 | [SIGN_CONFIG_KEY]?: Record; 189 | [SIGNED_URL_KEY]: { 190 | [key in T]: U extends { sign: RequestPresigningArguments } 191 | ? string[] 192 | : undefined; 193 | }; 194 | }; 195 | }> => { 196 | return async (c, next) => { 197 | c.set(SIGNED_URL_KEY, { 198 | ...(c.get(SIGNED_URL_KEY) ?? {}), 199 | }); 200 | 201 | if (options?.sign) { 202 | c.set(SIGN_CONFIG_KEY, { 203 | ...(c.get(SIGN_CONFIG_KEY) ?? {}), 204 | [name]: options.sign, 205 | }); 206 | } 207 | 208 | await this.storage.multiple(name)(c as Context, next); 209 | }; 210 | }; 211 | 212 | fields = >( 213 | schema: T, 214 | ): MiddlewareHandler<{ 215 | Variables: { 216 | [FILES_KEY]: { 217 | [key in keyof T]: T[key]["type"] extends "single" 218 | ? FieldValue | undefined 219 | : FieldValue[]; 220 | }; 221 | [SIGN_CONFIG_KEY]?: Record; 222 | [SIGNED_URL_KEY]: { 223 | [key in keyof T]: T[key]["type"] extends "single" 224 | ? T[key] extends { sign: RequestPresigningArguments } 225 | ? string 226 | : undefined 227 | : T[key] extends { sign: RequestPresigningArguments } 228 | ? string[] 229 | : undefined; 230 | }; 231 | }; 232 | }> => { 233 | return async (c, next) => { 234 | c.set(SIGNED_URL_KEY, { 235 | ...(c.get(SIGNED_URL_KEY) ?? {}), 236 | }); 237 | 238 | if (Object.keys(schema).some((key) => schema[key].sign)) { 239 | c.set(SIGN_CONFIG_KEY, { 240 | ...(c.get(SIGN_CONFIG_KEY) ?? {}), 241 | ...Object.keys(schema).reduce( 242 | (acc, key) => { 243 | if (schema[key].sign) { 244 | acc[key] = schema[key].sign as RequestPresigningArguments; 245 | } 246 | return acc; 247 | }, 248 | {} as Record, 249 | ), 250 | }); 251 | } 252 | 253 | await this.storage.fields(schema)(c as Context, next); 254 | }; 255 | }; 256 | } 257 | -------------------------------------------------------------------------------- /packages/s3/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetObjectCommand, 3 | PutObjectCommand, 4 | S3Client, 5 | } from "@aws-sdk/client-s3"; 6 | import { FieldValue } from "@hono-storage/core"; 7 | import { RequestPresigningArguments } from "@smithy/types"; 8 | import { File } from "@web-std/file"; 9 | import { Hono } from "hono"; 10 | 11 | import { HonoS3Storage } from "../"; 12 | import { BaseHonoS3Storage, IS3Object, IS3Sign } from "../src/storage"; 13 | 14 | describe("HonoS3Storage", () => { 15 | const putToS3 = vi.fn(); 16 | const getSignedURLFromS3 = vi.fn(); 17 | let app = new Hono(); 18 | 19 | class MockS3Repository implements IS3Object, IS3Sign { 20 | async put(command: PutObjectCommand) { 21 | putToS3(command); 22 | } 23 | async getSingedURL( 24 | command: GetObjectCommand, 25 | sign: RequestPresigningArguments, 26 | ) { 27 | getSignedURLFromS3(command, sign); 28 | return "https://example.com"; 29 | } 30 | } 31 | 32 | beforeEach(() => { 33 | app = new Hono(); 34 | }); 35 | 36 | afterEach(() => { 37 | putToS3.mockClear(); 38 | getSignedURLFromS3.mockClear(); 39 | }); 40 | 41 | it("should be able to create a new instance", () => { 42 | const client = new S3Client(); 43 | const storage = new HonoS3Storage({ 44 | bucket: "hono-storage", 45 | client, 46 | }); 47 | 48 | expect(storage).toBeInstanceOf(HonoS3Storage); 49 | }); 50 | 51 | describe("single", () => { 52 | it("should work", async () => { 53 | let actualFile: unknown; 54 | let actualSignedURL: unknown; 55 | const storage = new BaseHonoS3Storage( 56 | { 57 | bucket: "hono-storage", 58 | client: new S3Client(), 59 | }, 60 | new MockS3Repository(), 61 | ); 62 | 63 | app.post("/", storage.single("image"), (c) => { 64 | expectTypeOf(c.var.files.image).toMatchTypeOf(); 65 | actualFile = c.var.files.image; 66 | expectTypeOf(c.var.signedURLs.image).toMatchTypeOf(); 67 | actualSignedURL = c.var.signedURLs.image; 68 | return c.text("OK"); 69 | }); 70 | 71 | const form = new FormData(); 72 | form.append("image", new File([], "sample1.png")); 73 | 74 | await app.request("http://localhost", { 75 | method: "POST", 76 | body: form, 77 | }); 78 | 79 | expect(putToS3).toHaveBeenCalledTimes(1); 80 | expect(actualFile).toBeInstanceOf(File); 81 | expect(actualSignedURL).toBe(undefined); 82 | }); 83 | 84 | it("should work with sign option", async () => { 85 | let actualFile: unknown; 86 | let actualSignedURL: unknown; 87 | const storage = new BaseHonoS3Storage( 88 | { 89 | bucket: "hono-storage", 90 | client: new S3Client(), 91 | }, 92 | new MockS3Repository(), 93 | ); 94 | 95 | app.post( 96 | "/", 97 | storage.single("image", { sign: { expiresIn: 60 } }), 98 | (c) => { 99 | expectTypeOf(c.var.files.image).toMatchTypeOf< 100 | FieldValue | undefined 101 | >(); 102 | expectTypeOf(c.var.signedURLs.image).toMatchTypeOf< 103 | string | undefined 104 | >(); 105 | actualFile = c.var.files.image; 106 | actualSignedURL = c.var.signedURLs.image; 107 | return c.text("OK"); 108 | }, 109 | ); 110 | 111 | const form = new FormData(); 112 | form.append("image", new File([], "sample1.png")); 113 | 114 | await app.request("http://localhost", { 115 | method: "POST", 116 | body: form, 117 | }); 118 | 119 | expect(putToS3).toHaveBeenCalledTimes(1); 120 | expect(getSignedURLFromS3).toHaveBeenCalledTimes(1); 121 | expect(getSignedURLFromS3.mock.calls[0][1]).toMatchObject({ 122 | expiresIn: 60, 123 | }); 124 | expect(actualFile).toBeInstanceOf(File); 125 | expect(actualSignedURL).toBe("https://example.com"); 126 | }); 127 | }); 128 | 129 | describe("multiple", () => { 130 | it("should work", async () => { 131 | let actualFiles: unknown; 132 | let actualSignedURL: unknown; 133 | const storage = new BaseHonoS3Storage( 134 | { 135 | bucket: "hono-storage", 136 | client: new S3Client(), 137 | }, 138 | new MockS3Repository(), 139 | ); 140 | 141 | app.post("/", storage.multiple("images"), (c) => { 142 | expectTypeOf(c.var.files.images).toMatchTypeOf< 143 | FieldValue | undefined 144 | >(); 145 | actualFiles = c.var.files.images; 146 | expectTypeOf(c.var.signedURLs.images).toMatchTypeOf(); 147 | actualSignedURL = c.var.signedURLs.images; 148 | return c.text("OK"); 149 | }); 150 | 151 | const form = new FormData(); 152 | form.append("images", new File([], "sample1.png")); 153 | form.append("images", new File([], "sample2.png")); 154 | 155 | await app.request("http://localhost", { 156 | method: "POST", 157 | body: form, 158 | }); 159 | 160 | expect(putToS3).toHaveBeenCalledTimes(2); 161 | expect(getSignedURLFromS3).toHaveBeenCalledTimes(0); 162 | expect(actualFiles).toBeInstanceOf(Array); 163 | expect(actualFiles).toHaveLength(2); 164 | assert( 165 | Array.isArray(actualFiles) && 166 | actualFiles.every((v) => v instanceof File), 167 | ); 168 | expect(actualSignedURL).toBe(undefined); 169 | }); 170 | 171 | it("should work with sign option", async () => { 172 | let actualFiles: unknown; 173 | let actualSignedURL: unknown; 174 | const storage = new BaseHonoS3Storage( 175 | { 176 | bucket: "hono-storage", 177 | client: new S3Client(), 178 | }, 179 | new MockS3Repository(), 180 | ); 181 | 182 | app.post( 183 | "/", 184 | storage.multiple("images", { sign: { expiresIn: 60 } }), 185 | (c) => { 186 | expectTypeOf(c.var.files.images).toMatchTypeOf< 187 | FieldValue | undefined 188 | >(); 189 | expectTypeOf(c.var.signedURLs.images).toMatchTypeOf(); 190 | actualFiles = c.var.files.images; 191 | actualSignedURL = c.var.signedURLs.images; 192 | return c.text("OK"); 193 | }, 194 | ); 195 | 196 | const form = new FormData(); 197 | form.append("images", new File([], "sample1.png")); 198 | form.append("images", new File([], "sample2.png")); 199 | 200 | await app.request("http://localhost", { 201 | method: "POST", 202 | body: form, 203 | }); 204 | 205 | expect(putToS3).toHaveBeenCalledTimes(2); 206 | expect(getSignedURLFromS3).toHaveBeenCalledTimes(2); 207 | expect(getSignedURLFromS3.mock.calls[0][1]).toMatchObject({ 208 | expiresIn: 60, 209 | }); 210 | expect(getSignedURLFromS3.mock.calls[1][1]).toMatchObject({ 211 | expiresIn: 60, 212 | }); 213 | expect(actualFiles).toBeInstanceOf(Array); 214 | expect(actualFiles).toHaveLength(2); 215 | assert( 216 | Array.isArray(actualFiles) && 217 | actualFiles.every((v) => v instanceof File), 218 | ); 219 | expect(actualSignedURL).toBeInstanceOf(Array); 220 | expect(actualSignedURL).toHaveLength(2); 221 | assert( 222 | Array.isArray(actualSignedURL) && 223 | actualSignedURL.every((v) => v === "https://example.com"), 224 | ); 225 | }); 226 | }); 227 | 228 | describe("fields", () => { 229 | it("should work", async () => { 230 | let actualImage: unknown; 231 | let actualImages: unknown; 232 | let actualSignedURL: unknown; 233 | const storage = new BaseHonoS3Storage( 234 | { 235 | bucket: "hono-storage", 236 | client: new S3Client(), 237 | }, 238 | new MockS3Repository(), 239 | ); 240 | 241 | app.post( 242 | "/", 243 | storage.fields({ 244 | image: { 245 | type: "single", 246 | }, 247 | images: { 248 | type: "multiple", 249 | }, 250 | }), 251 | (c) => { 252 | expectTypeOf(c.var.files.image).toMatchTypeOf< 253 | FieldValue | undefined 254 | >(); 255 | expectTypeOf(c.var.files.images).toMatchTypeOf(); 256 | actualImage = c.var.files.image; 257 | actualImages = c.var.files.images; 258 | expectTypeOf(c.var.signedURLs.image).toMatchTypeOf(); 259 | expectTypeOf(c.var.signedURLs.images).toMatchTypeOf(); 260 | actualSignedURL = c.var.signedURLs; 261 | return c.text("OK"); 262 | }, 263 | ); 264 | 265 | const form = new FormData(); 266 | form.append("image", new File([], "sample1.png")); 267 | form.append("images", new File([], "sample2.png")); 268 | form.append("images", new File([], "sample3.png")); 269 | 270 | await app.request("http://localhost", { 271 | method: "POST", 272 | body: form, 273 | }); 274 | 275 | expect(putToS3).toHaveBeenCalledTimes(3); 276 | expect(getSignedURLFromS3).toHaveBeenCalledTimes(0); 277 | expect(actualImage).toBeInstanceOf(File); 278 | expect(actualImages).toBeInstanceOf(Array); 279 | expect(actualImages).toHaveLength(2); 280 | assert( 281 | Array.isArray(actualImages) && 282 | actualImages.every((v) => v instanceof File), 283 | ); 284 | expect(actualSignedURL).toBeInstanceOf(Object); 285 | }); 286 | 287 | it("should work with sign option", async () => { 288 | let actualImage: unknown; 289 | let actualImages: unknown; 290 | let actualSignedURL: unknown; 291 | const storage = new BaseHonoS3Storage( 292 | { 293 | bucket: "hono-storage", 294 | client: new S3Client(), 295 | }, 296 | new MockS3Repository(), 297 | ); 298 | 299 | app.post( 300 | "/", 301 | storage.fields({ 302 | image: { 303 | type: "single", 304 | sign: { expiresIn: 60 }, 305 | }, 306 | images: { 307 | type: "multiple", 308 | sign: { expiresIn: 60 }, 309 | }, 310 | }), 311 | (c) => { 312 | expectTypeOf(c.var.files.image).toMatchTypeOf< 313 | FieldValue | undefined 314 | >(); 315 | expectTypeOf(c.var.files.images).toMatchTypeOf(); 316 | expectTypeOf(c.var.signedURLs.image).toMatchTypeOf< 317 | string | undefined 318 | >(); 319 | expectTypeOf(c.var.signedURLs.images).toMatchTypeOf(); 320 | actualImage = c.var.files.image; 321 | actualImages = c.var.files.images; 322 | actualSignedURL = c.var.signedURLs; 323 | return c.text("OK"); 324 | }, 325 | ); 326 | 327 | const form = new FormData(); 328 | form.append("image", new File([], "sample1.png")); 329 | form.append("images", new File([], "sample2.png")); 330 | form.append("images", new File([], "sample3.png")); 331 | 332 | await app.request("http://localhost", { 333 | method: "POST", 334 | body: form, 335 | }); 336 | 337 | expect(putToS3).toHaveBeenCalledTimes(3); 338 | expect(getSignedURLFromS3).toHaveBeenCalledTimes(3); 339 | expect(getSignedURLFromS3.mock.calls[0][1]).toMatchObject({ 340 | expiresIn: 60, 341 | }); 342 | expect(getSignedURLFromS3.mock.calls[1][1]).toMatchObject({ 343 | expiresIn: 60, 344 | }); 345 | expect(actualImage).toBeInstanceOf(File); 346 | expect(actualImages).toBeInstanceOf(Array); 347 | expect(actualImages).toHaveLength(2); 348 | assert( 349 | Array.isArray(actualImages) && 350 | actualImages.every((v) => v instanceof File), 351 | ); 352 | expect(actualSignedURL).toBeInstanceOf(Object); 353 | expect(actualSignedURL).toMatchObject({ 354 | image: "https://example.com", 355 | images: ["https://example.com", "https://example.com"], 356 | }); 357 | }); 358 | }); 359 | }); 360 | -------------------------------------------------------------------------------- /packages/s3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./src", 6 | "types": ["vitest/globals"] 7 | }, 8 | "include": ["./src/**/*.ts", "./tests/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/s3/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | - "website" 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^build"] 11 | }, 12 | "lint:check": { 13 | "dependsOn": ["^build"] 14 | }, 15 | "format": {}, 16 | "format:check": {}, 17 | "test": { 18 | "dependsOn": ["^build"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | docs/.vitepress/cache 27 | -------------------------------------------------------------------------------- /website/docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.vuejs.org/config/app-configs 4 | export default defineConfig({}) 5 | -------------------------------------------------------------------------------- /website/docs/index.md: -------------------------------------------------------------------------------- 1 | # Hello Hono Storage 2 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-storage-website", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vitepress dev docs", 7 | "build": "vitepress build docs", 8 | "serve": "vitepress serve docs" 9 | }, 10 | "devDependencies": { 11 | "vitepress": "1.0.0-rc.25", 12 | "vue": "^3.3.7" 13 | } 14 | } 15 | --------------------------------------------------------------------------------