index.ts
Raw
1import { Database } from "bun:sqlite";
2import cron from "@elysiajs/cron";
3import { html } from "@elysiajs/html";
4import staticPlugin from "@elysiajs/static";
5import { randomUUIDv7 } from "bun";
6import { Elysia, StatusMap, t } from "elysia";
7import {
8 filetypes,
9 Index,
10 NotFound,
11 SetCookie,
12 ShowFile,
13 WrongPassword,
14} from "./components";
15import { decrypt, encrypt } from "./crypto";
16
17const db = new Database("./db/db.sqlite");
18db.run("PRAGMA foreign_keys = ON");
19db.run("PRAGMA journal_mode = WAL2");
20db.run(
21 "CREATE TABLE IF NOT EXISTS files (uuid TEXT PRIMARY KEY, filename TEXT NOT NULL, content BLOB NOT NULL, filetype TEXT NOT NULL, encrypted INTEGER NOT NULL, delete_at INTEGER) STRICT",
22);
23db.run("PRAGMA optimize");
24
25function stringArrayToEnum<T extends string>(
26 arr: readonly T[],
27): { [K in T]: K } {
28 return arr.reduce((acc, key) => {
29 acc[key] = key;
30 return acc;
31 }, Object.create(null));
32}
33
34const app = new Elysia({
35 serve: {
36 maxRequestBodySize: 1024 * 1024 * 1024, // 1GB
37 },
38})
39 .use(staticPlugin({ assets: "./assets", prefix: "/" }))
40 .use(html())
41 .use(
42 cron({
43 name: "delete",
44 pattern: "*/5 * * * * *",
45 run() {
46 db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')");
47 },
48 }),
49 )
50 .get("/", ({ server }) => Index(server?.url.toString() ?? ""))
51 .post(
52 "/upload",
53 async ({ set, body }) => {
54 const uuid = randomUUIDv7();
55 let content: Uint8Array = Buffer.from(await body.file.bytes());
56 let encrypted = false;
57 let delete_at: number | null = null;
58 if (body.delete_in_minutes) {
59 delete_at =
60 Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60;
61 }
62 if (body.encrypted === "on") {
63 encrypted = true;
64 } else if (body.password) {
65 content = await encrypt(content, body.password);
66 encrypted = true;
67 }
68 db.exec(
69 "INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
70 [
71 uuid,
72 body.filename || body.file.name,
73 content,
74 body.filetype,
75 encrypted,
76 delete_at,
77 ],
78 );
79 set.status = StatusMap["See Other"];
80 set.headers.location = `/show/${uuid}`;
81 return `Created with id: ${uuid}`;
82 },
83 {
84 body: t.Object({
85 file: t.File(),
86 filename: t.Optional(t.String()),
87 //filetype: t.String()/*todo:verifiy via t.Enum*/,
88 filetype: t.Enum(stringArrayToEnum(filetypes)),
89 password: t.Optional(t.String()),
90 encrypted: t.Optional(t.String()),
91 encrypt_mode: t.Optional(t.String()),
92 delete_in_minutes: t.String({
93 format: "regex",
94 pattern: "(^$|^[0-9]+$)",
95 }),
96 }),
97 },
98 )
99 .get(
100 "/show/:uuid",
101 async ({ set, params, cookie }) => {
102 const result =
103 (db
104 .prepare(
105 "SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?",
106 )
107 .get(params.uuid) as {
108 filename: string;
109 content: Uint8Array;
110 filetype: string;
111 encrypted: boolean;
112 delete_at: number | null;
113 }) || null;
114 if (!result) {
115 set.status = StatusMap["Not Found"];
116 return NotFound();
117 }
118 if (result.encrypted) {
119 const password = cookie.password.value;
120 if (!password) {
121 return ShowFile(
122 result.filename,
123 params.uuid,
124 null,
125 result.filetype,
126 result.delete_at,
127 );
128 } else {
129 try {
130 result.content = await decrypt(result.content, password);
131 } catch (_e) {
132 set.status = StatusMap.Forbidden;
133 set.headers["set-cookie"] = [
134 `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
135 `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
136 ];
137 return WrongPassword();
138 }
139 }
140 }
141 return ShowFile(
142 result.filename,
143 params.uuid,
144 result.content,
145 result.filetype,
146 result.delete_at,
147 );
148 },
149 {
150 params: t.Object({ uuid: t.String() }),
151 cookie: t.Object({ password: t.Optional(t.String()) }),
152 },
153 )
154 .post(
155 "/set-cookie/:uuid",
156 ({ set, body, params }) => {
157 set.headers["set-cookie"] = [
158 `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
159 `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`,
160 ];
161 set.headers.location = `/show/${params.uuid}`;
162 set.status = StatusMap["See Other"];
163 return SetCookie(params.uuid);
164 },
165 {
166 body: t.Object({
167 password: t.String(),
168 decrypt_mode: t.Optional(t.String()),
169 }),
170 params: t.Object({ uuid: t.String() }),
171 },
172 )
173 .get(
174 "/raw/:uuid",
175 async ({ set, params, cookie, query }) => {
176 const result =
177 (db
178 .prepare(
179 "SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?",
180 )
181 .get(params.uuid) as {
182 content: Uint8Array;
183 filename: string;
184 encrypted: boolean;
185 filetype: string;
186 }) || null;
187 if (!result) {
188 set.status = StatusMap["Not Found"];
189 return "File not found";
190 }
191 if (result.encrypted && query.ignore_password !== "true") {
192 if (!cookie.password.value) {
193 set.status = StatusMap.Unauthorized;
194 return 'This file is encrypted, set the cookie "password" with the correct password to allow the server to decrypt it';
195 }
196 try {
197 result.content = await decrypt(result.content, cookie.password.value);
198 } catch (_e) {
199 set.status = StatusMap.Forbidden;
200 return "Incorrect password";
201 }
202 }
203 set.headers.encrypted = result.encrypted ? "true" : "false";
204 set.headers.filetype = result.filetype;
205 set.headers.filename = result.filename;
206 set.headers["content-disposition"] =
207 `inline; filename=${result.filename}`;
208 return result.content;
209 },
210 {
211 params: t.Object({ uuid: t.String() }),
212 cookie: t.Object({ password: t.Optional(t.String()) }),
213 query: t.Object({ ignore_password: t.Optional(t.String()) }),
214 },
215 )
216 .listen(3000);
217
218console.log(
219 `⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`,
220);
221