Skip to content

Commit 6175dce

Browse files
authored
Merge pull request #1 from mojisdev/upload-files
feat: add stow route for file uploads with tar.gz parsing
2 parents 0c3501b + d60c72f commit 6175dce

File tree

6 files changed

+99
-38
lines changed

6 files changed

+99
-38
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"dependencies": {
1717
"@hono/arktype-validator": "^2.0.0",
1818
"arktype": "^2.1.9",
19-
"hono": "^4.7.4"
19+
"hono": "^4.7.4",
20+
"nanotar": "^0.2.0"
2021
},
2122
"devDependencies": {
2223
"@cloudflare/vitest-pool-workers": "^0.8.0",

pnpm-lock.yaml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hashes.ts

+15-36
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { HonoEnv } from "./types";
22
import { arktypeValidator } from "@hono/arktype-validator";
33
import { type } from "arktype";
44
import { Hono } from "hono";
5-
import { HTTPException } from "hono/http-exception";
65
import { authMiddleware } from "./middlewares/auth";
76

87
export const HASHES_ROUTER = new Hono<HonoEnv>().basePath("/hashes");
@@ -11,9 +10,22 @@ function getKVPrefix(env: string): string {
1110
return env === "production" ? "prod" : env === "preview" ? "preview" : "dev";
1211
}
1312

14-
HASHES_ROUTER.get("/", async (c) => {
13+
HASHES_ROUTER.get("/delete", authMiddleware, async (c) => {
1514
const result = await c.env.MOJIS_HASHES.list({
16-
prefix: getKVPrefix(c.env.ENVIRONMENT),
15+
prefix: `${getKVPrefix(c.env.ENVIRONMENT)}`,
16+
});
17+
18+
const keys = result.keys.map((h) => h.name);
19+
await Promise.all(keys.map((key) => c.env.MOJIS_HASHES.delete(key)));
20+
21+
return c.json({
22+
done: true,
23+
});
24+
});
25+
26+
HASHES_ROUTER.get("/:version", async (c) => {
27+
const result = await c.env.MOJIS_HASHES.list({
28+
prefix: `${getKVPrefix(c.env.ENVIRONMENT)}:${c.req.param("version")}`,
1729
});
1830

1931
const keys = result.keys.map((h) => h.name);
@@ -35,39 +47,6 @@ HASHES_ROUTER.get("/", async (c) => {
3547
return c.json(hashes.filter((h) => h != null));
3648
});
3749

38-
HASHES_ROUTER.get("/:version", async (c) => {
39-
const version = c.req.param("version");
40-
41-
const hash = await c.env.MOJIS_HASHES.get(`${getKVPrefix(c.env.ENVIRONMENT)}:${version}`);
42-
43-
if (hash == null) {
44-
throw new HTTPException(404, {
45-
message: "Hash not found",
46-
});
47-
}
48-
49-
return c.json({
50-
hash,
51-
});
52-
});
53-
54-
HASHES_ROUTER.get("/:version/:item", async (c) => {
55-
const version = c.req.param("version");
56-
const item = c.req.param("item");
57-
58-
const hash = await c.env.MOJIS_HASHES.get(`${getKVPrefix(c.env.ENVIRONMENT)}:${version}:${item}`);
59-
60-
if (hash == null) {
61-
throw new HTTPException(404, {
62-
message: "Hash not found",
63-
});
64-
}
65-
66-
return c.json({
67-
hash,
68-
});
69-
});
70-
7150
HASHES_ROUTER.post(
7251
"/",
7352
authMiddleware,

src/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Hono } from "hono";
33
import { cache } from "hono/cache";
44
import { HTTPException } from "hono/http-exception";
55
import { HASHES_ROUTER } from "./hashes";
6+
import { STOW_ROUTER } from "./stow";
67

78
const app = new Hono<HonoEnv>();
89

@@ -15,6 +16,7 @@ app.get(
1516
);
1617

1718
app.route("/", HASHES_ROUTER);
19+
app.route("/", STOW_ROUTER);
1820

1921
app.onError(async (err, c) => {
2022
console.error(err);
@@ -46,4 +48,10 @@ app.notFound(async (c) => {
4648
} satisfies ApiError, 404);
4749
});
4850

49-
export default app;
51+
export default {
52+
fetch: app.fetch,
53+
scheduled: async (_ctrl, _env, _ctx) => {
54+
// eslint-disable-next-line no-console
55+
console.log("Scheduled task");
56+
},
57+
} satisfies ExportedHandler<HonoEnv>;

src/stow.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Hono } from "hono";
2+
import { HTTPException } from "hono/http-exception";
3+
import { parseTarGzip } from "nanotar";
4+
import { authMiddleware } from "./middlewares/auth";
5+
6+
export const STOW_ROUTER = new Hono().basePath("/stow");
7+
8+
STOW_ROUTER.post(
9+
"/:version",
10+
authMiddleware,
11+
async (c) => {
12+
const { version } = c.req.param();
13+
const body = await c.req.parseBody();
14+
15+
const file = Array.isArray(body.file) ? body.file[0] : body.file;
16+
17+
if (file == null) {
18+
throw new HTTPException(400, {
19+
message: "No file uploaded",
20+
});
21+
}
22+
23+
if (typeof file === "string") {
24+
throw new HTTPException(400, {
25+
message: "invalid file uploaded",
26+
});
27+
}
28+
29+
const tar = await parseTarGzip(await file.arrayBuffer(), {
30+
filter(file) {
31+
// if file is pax header, return false
32+
if (Number.isNaN(file.type)) return false;
33+
if (file.name.includes("PaxHeader")) return false;
34+
// some files is prefixed with ._, ignore them
35+
if (file.name.includes("._")) return false;
36+
return true;
37+
},
38+
});
39+
40+
const promises = [];
41+
42+
for (const entry of tar) {
43+
if (entry.type !== "file") continue;
44+
const normalizedEntryName = entry.name.replace("./", "");
45+
// eslint-disable-next-line no-console
46+
console.log(
47+
`Uploading ${entry.name} (${entry.size} bytes) to ${version}/${normalizedEntryName}`,
48+
);
49+
promises.push(c.env.EMOJI_DATA.put(`${version}/${normalizedEntryName}`, entry.text));
50+
}
51+
52+
c.executionCtx.waitUntil(
53+
Promise.all(promises),
54+
);
55+
56+
return c.json({
57+
message: "Files uploaded",
58+
});
59+
},
60+
);

wrangler.jsonc

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"vars": {
2727
"MOJIS_TOKEN": ""
2828
},
29+
"triggers": {
30+
"crons": [
31+
"0 */12 * * *"
32+
]
33+
},
2934
"placement": { "mode": "smart" },
3035
"env": {
3136
"preview": {

0 commit comments

Comments
 (0)