├── .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 |
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 |
--------------------------------------------------------------------------------