Skip to content

Commit

Permalink
Merge pull request #38 from sor4chi/feat/signed-url
Browse files Browse the repository at this point in the history
feat: signed url support for S3 / R2
  • Loading branch information
sor4chi authored Dec 9, 2023
2 parents f324dda + 1d21fae commit cd203aa
Show file tree
Hide file tree
Showing 14 changed files with 944 additions and 132 deletions.
38 changes: 38 additions & 0 deletions .changeset/hungry-ties-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@hono-storage/s3": patch
---

feat: support `signedUrl` for s3 storage.

```ts
import { S3Client } from "@aws-sdk/client-s3";
import { HonoS3Storage } from "@hono-storage/s3";
import { Hono } from "hono";

const client = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: "...",
secretAccessKey: "...",
},
});

const storage = new HonoS3Storage({
bucket: "hono-storage",
client,
});

const app = new Hono();

app.post(
"/",
storage.single("image", {
sign: {
expiresIn: 60,
},
}),
(c) => {
return c.json({ signedURL: c.var.signedURLs.image });
},
);
```
5 changes: 5 additions & 0 deletions .changeset/tall-points-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hono-storage/core": patch
---

feat: add `field` property to `HonoStorageFile`(HSF).
2 changes: 2 additions & 0 deletions examples/s3-sign/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
8 changes: 8 additions & 0 deletions examples/s3-sign/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```
npm install
npm run start
```

```
open http://localhost:3000
```
18 changes: 18 additions & 0 deletions examples/s3-sign/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@hono-storage/s3-sign-example",
"private": true,
"version": "0.0.0",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.428.0",
"@hono-storage/s3": "workspace:*",
"@hono/node-server": "^1.1.0",
"dotenv": "^16.3.1",
"hono": "^3.8.1"
},
"devDependencies": {
"tsx": "^3.12.2"
}
}
60 changes: 60 additions & 0 deletions examples/s3-sign/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { S3Client } from "@aws-sdk/client-s3";
import { serve } from "@hono/node-server";
import { HonoS3Storage } from "@hono-storage/s3";
import { config } from "dotenv";
import { Hono } from "hono";
import { html } from "hono/html";

config();

const app = new Hono();

if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
throw new Error("AWS credentials not found");
}

const client = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});

const storage = new HonoS3Storage({
key: (_, file) =>
`${file.originalname}-${new Date().getTime()}.${file.extension}`,
bucket: "hono-storage",
client,
});

let signedURL = "";

app.post(
"/",
storage.single("image", {
sign: {
expiresIn: 60,
},
}),
(c) => {
signedURL = c.var.signedURLs.image || "";
return c.html(html`
<a href="/">Back</a>
<p>Image uploaded successfully</p>
<code>${signedURL}</code>
`);
},
);

app.get("/", (c) =>
c.html(html`
<form action="/" method="POST" enctype="multipart/form-data">
<input type="file" name="image" />
<button type="submit">Submit</button>
</form>
<img src="${signedURL}" />
`),
);

serve(app);
10 changes: 9 additions & 1 deletion packages/core/src/file.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { File } from "@web-std/file";

type Field = {
name: string;
type: "single" | "multiple";
};

export class HonoStorageFile extends File {
constructor(file: Blob | File) {
field: Field;

constructor(file: Blob | File, field: Field) {
super([file], file.name);
this.field = field;
}

get originalname(): string {
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,33 @@ export class HonoStorage {
private handleSingleStorage = async (
c: Context,
file: File,
fieldName: string,
): Promise<void> => {
if (this.options.storage) {
await this.options.storage(c, [new HonoStorageFile(file)]);
await this.options.storage(c, [
new HonoStorageFile(file, {
name: fieldName,
type: "single",
}),
]);
}
};

private handleMultipleStorage = async (
c: Context,
files: File[],
fieldName: string,
): Promise<void> => {
if (this.options.storage) {
await this.options.storage(
c,
files.map((file) => new HonoStorageFile(file)),
files.map(
(file) =>
new HonoStorageFile(file, {
name: fieldName,
type: "multiple",
}),
),
);
}
};
Expand All @@ -73,7 +86,7 @@ export class HonoStorage {
const formData = await c.req.parseBody({ all: true });
const value = formData[name];
if (isFile(value)) {
await this.handleSingleStorage(c, value);
await this.handleSingleStorage(c, value, name);
}

c.set(FILES_KEY, {
Expand Down Expand Up @@ -110,7 +123,7 @@ export class HonoStorage {
throw new Error("Too many files");
}

await this.handleMultipleStorage(c, filedFiles);
await this.handleMultipleStorage(c, filedFiles, name);

c.set(FILES_KEY, {
...c.get(FILES_KEY),
Expand Down Expand Up @@ -152,14 +165,14 @@ export class HonoStorage {
if (field.maxCount && filedFiles.length > field.maxCount) {
throw new Error("Too many files");
}
uploader.push(this.handleMultipleStorage(c, filedFiles));
uploader.push(this.handleMultipleStorage(c, filedFiles, name));
files[name] = [value].flat();
continue;
}

if (field.type === "single") {
if (isFile(value)) {
uploader.push(this.handleSingleStorage(c, value));
uploader.push(this.handleSingleStorage(c, value, name));
}
files[name] = value;
continue;
Expand Down
35 changes: 28 additions & 7 deletions packages/core/tests/file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,61 @@ import { HonoStorageFile } from "../src/file";

describe("HonoStorageFile", () => {
it("should be able to create a new instance", () => {
const file = new HonoStorageFile(new File([], "sample1.txt"));
const file = new HonoStorageFile(new File([], "sample1.txt"), {
name: "file",
type: "single",
});
expect(file).toBeInstanceOf(HonoStorageFile);
});

describe("originalname", () => {
it("should work with file without extension", () => {
const file = new HonoStorageFile(new File([], "sample1"));
const file = new HonoStorageFile(new File([], "sample1"), {
name: "file",
type: "single",
});
expect(file.originalname).toBe("sample1");
});

it("should work with file with extension", () => {
const file = new HonoStorageFile(new File([], "sample1.txt"));
const file = new HonoStorageFile(new File([], "sample1.txt"), {
name: "file",
type: "single",
});
expect(file.originalname).toBe("sample1");
});

it("should work with file with multiple dots", () => {
const file = new HonoStorageFile(new File([], "sample1.txt.zip"));
const file = new HonoStorageFile(new File([], "sample1.txt.zip"), {
name: "file",
type: "single",
});
expect(file.originalname).toBe("sample1.txt");
});
});

describe("extension", () => {
it("should work with file without extension", () => {
const file = new HonoStorageFile(new File([], "sample1"));
const file = new HonoStorageFile(new File([], "sample1"), {
name: "file",
type: "single",
});
expect(file.extension).toBe("");
});

it("should work with file with extension", () => {
const file = new HonoStorageFile(new File([], "sample1.txt"));
const file = new HonoStorageFile(new File([], "sample1.txt"), {
name: "file",
type: "single",
});
expect(file.extension).toBe("txt");
});

it("should work with file with multiple dots", () => {
const file = new HonoStorageFile(new File([], "sample1.txt.zip"));
const file = new HonoStorageFile(new File([], "sample1.txt.zip"), {
name: "file",
type: "single",
});
expect(file.extension).toBe("zip");
});
});
Expand Down
5 changes: 5 additions & 0 deletions packages/s3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.428.0",
"@aws-sdk/s3-request-presigner": "3.428.0",
"@hono-storage/core": "workspace:*",
"hono": "^3.8.1"
},
"devDependencies": {
"@smithy/types": "^2.4.0",
"@web-std/file": "^3.0.3"
}
}
Loading

0 comments on commit cd203aa

Please sign in to comment.