biome integration & cleanup

AuthorKonata <konata@posteo.jp>
Date
Commit82a92c3cb14744ce391b1fa4dd72b788ee55c0d2
Parent0cd1857
12 files changed, 985 insertions(+), 707 deletions(-)
M.gitignore
@@ -1,5 +1,5 @@
11 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2-
2+/.idea
33 # dependencies
44 /node_modules
55 /.pnp
@@ -41,4 +41,4 @@ yarn-error.log*
4141 package-lock.json
4242 **/*.bun
4343 db
44-assets/dist
44+assets/dist
Massets/default.css
@@ -2,87 +2,86 @@
22 @import "sakura-vader.css";
33
44 #jsguard {
5- display: none;
5+ display: none;
66 }
77
88 body {
9- display: flex;
9+ display: flex;
1010 align-items: center;
1111 flex-direction: column;
1212 min-height: 100vh;
1313
14- padding: 3rem;
14+ padding: 3rem;
1515 box-sizing: border-box;
16- max-width: 80rem;
16+ max-width: 80rem;
1717 }
1818
1919 a {
20- text-wrap: nowrap;
20+ text-wrap: nowrap;
2121 }
2222
2323 hr {
24- width: 100%;
24+ width: 100%;
2525 }
2626
2727 h1 {
28- margin-bottom: 4rem;
28+ margin-bottom: 4rem;
2929 }
3030
3131 form {
32- display: flex;
33- flex-direction: column;
34- gap: 0.5rem;
35- margin-bottom: 1.5rem;
36- min-width: 35rem;
37- width: 100%;
38-
39- input {
40- flex-grow: 1;
41- }
42-
43- input[type=checkbox] {
44- flex-grow: 0;
45- margin-bottom: 0.5rem;
46- }
47-
48- div {
49- display: flex;
50- flex-direction: row;
51- align-items: center;
52- gap: 1rem;
53- }
32+ display: flex;
33+ flex-direction: column;
34+ gap: 0.5rem;
35+ margin-bottom: 1.5rem;
36+ min-width: 35rem;
37+ width: 100%;
38+
39+ input {
40+ flex-grow: 1;
41+ }
42+
43+ input[type="checkbox"] {
44+ flex-grow: 0;
45+ margin-bottom: 0.5rem;
46+ }
47+
48+ div {
49+ display: flex;
50+ flex-direction: row;
51+ align-items: center;
52+ gap: 1rem;
53+ }
5454 }
5555
5656 select {
57- overflow: auto;
58- flex-grow: 1;
57+ overflow: auto;
58+ flex-grow: 1;
5959 }
6060
6161 img {
62- margin-bottom: 0;
62+ margin-bottom: 0;
6363 }
6464
6565 .help-icon {
66- width: 2rem;
67- height: 2rem;
68- filter: invert();
69- margin-left: 0.5rem;
66+ width: 2rem;
67+ height: 2rem;
68+ filter: invert();
69+ margin-left: 0.5rem;
7070 }
7171
72-input[type=reset] {
73- max-width: 25rem;
74- align-self: center;
72+input[type="reset"] {
73+ max-width: 25rem;
74+ align-self: center;
7575 }
7676
77-
7877 dialog {
79- background-color: #120c0e;
80- color: #d9d8dc;
81- border-color: #eb99a1;
82-
83- form {
84- display: flex;
85- flex-direction: column;
86- align-items: center;
87- }
88-}
78+ background-color: #120c0e;
79+ color: #d9d8dc;
80+ border-color: #eb99a1;
81+
82+ form {
83+ display: flex;
84+ flex-direction: column;
85+ align-items: center;
86+ }
87+}
Massets/show.css
@@ -3,92 +3,91 @@
33 @import "highlight.css";
44
55 hr {
6- width: 100%;
6+ width: 100%;
77 }
88
99 body {
10- box-sizing: border-box;
11- width: 100%;
12- height: 100vh;
13- max-width: 100%;
14- max-height: 100vh;
10+ box-sizing: border-box;
11+ width: 100%;
12+ height: 100vh;
13+ max-width: 100%;
14+ max-height: 100vh;
1515
16- display: flex;
17- flex-direction: row;
16+ display: flex;
17+ flex-direction: row;
1818
19- #content {
20- box-sizing: border-box;
21- flex-grow: 1;
22- height: 100%;
23- overflow: auto;
24- border: 1px solid #40363a;
25- border-right: unset;
26- display: flex;
27- flex-direction: column;
19+ #content {
20+ box-sizing: border-box;
21+ flex-grow: 1;
22+ height: 100%;
23+ overflow: auto;
24+ border: 1px solid #40363a;
25+ border-right: unset;
26+ display: flex;
27+ flex-direction: column;
2828
29- #filename {
30- padding-left: 1rem;
31- }
29+ #filename {
30+ padding-left: 1rem;
31+ }
3232
33- pre {
34- box-sizing: border-box;
35- margin: 0;
36- flex-grow: 1;
37- overflow: auto;
38- max-width: 100%;
39- max-height: 100%;
40- }
33+ pre {
34+ box-sizing: border-box;
35+ margin: 0;
36+ flex-grow: 1;
37+ overflow: auto;
38+ max-width: 100%;
39+ max-height: 100%;
40+ }
4141
42- #mediabox {
43- flex-grow: 1;
44- display: flex;
45- justify-content: center;
46- }
47- }
42+ #mediabox {
43+ flex-grow: 1;
44+ display: flex;
45+ justify-content: center;
46+ }
47+ }
4848
49- #sidebar {
50- box-sizing: border-box;
51- box-sizing: border-box;
52- flex-shrink: 0;
53- padding: 1rem;
54- width: 30rem;
55- height: 100%;
56- border: 1px solid #40363a;
57- display: flex;
58- flex-direction: column;
49+ #sidebar {
50+ box-sizing: border-box;
51+ flex-shrink: 0;
52+ padding: 1rem;
53+ width: 30rem;
54+ height: 100%;
55+ border: 1px solid #40363a;
56+ display: flex;
57+ flex-direction: column;
5958
60- h3 {
61- margin-top: 0;
62- text-align: center;
63- }
59+ h3 {
60+ margin-top: 0;
61+ text-align: center;
62+ }
6463
65- form {
66- align-self: center;
67- }
68-
69- }
64+ form {
65+ align-self: center;
66+ }
67+ }
7068 }
7169
7270 audio {
73- max-width: 40rem;
74- min-width: 0;
75- flex-grow: 1;
76- align-self: center;
71+ max-width: 40rem;
72+ min-width: 0;
73+ flex-grow: 1;
74+ align-self: center;
7775 }
7876
7977 img,
8078 video {
81- object-fit: scale-down;
79+ object-fit: scale-down;
8280 }
8381
8482 dialog {
85- background-color: #120c0e;
86- color: #d9d8dc;
87- border-color: #eb99a1;
83+ background-color: #120c0e;
84+ color: #d9d8dc;
85+ border-color: #eb99a1;
8886
89- form {
90- display: flex;
91- flex-direction: column;
92- align-items: center;
93- }
94-}
87+ /* biome-ignore lint/style/noDescendingSpecificity: targets dialog forms only, disjoint from the #sidebar form rule */
88+ form {
89+ display: flex;
90+ flex-direction: column;
91+ align-items: center;
92+ }
93+}
Abiome.json
@@ -0,0 +1,44 @@
1+{
2+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
3+ "vcs": {
4+ "enabled": true,
5+ "clientKind": "git",
6+ "useIgnoreFile": true
7+ },
8+ "files": {
9+ "ignoreUnknown": false,
10+ "includes": [
11+ "src/**",
12+ "*.json",
13+ "!bun.lock",
14+ "assets/show.css",
15+ "assets/default.css"
16+ ]
17+ },
18+ "formatter": {
19+ "enabled": true,
20+ "indentStyle": "tab"
21+ },
22+ "linter": {
23+ "enabled": true,
24+ "rules": {
25+ "recommended": true,
26+ "a11y": {
27+ "useMediaCaption": "off"
28+ }
29+ }
30+ },
31+ "javascript": {
32+ "formatter": {
33+ "quoteStyle": "double"
34+ }
35+ },
36+ "assist": {
37+ "enabled": true,
38+ "actions": {
39+ "source": {
40+ "organizeImports": "on"
41+ }
42+ }
43+ }
44+}
Mbun.lock
@@ -16,6 +16,7 @@
1616 "utf-8-validate": "^6.0.5",
1717 },
1818 "devDependencies": {
19+ "@biomejs/biome": "2.4.16",
1920 "@types/pkcs7-padding": "^0.1.3",
2021 "@types/utf-8-validate": "^5.0.2",
2122 "bun-types": "latest",
@@ -23,6 +24,24 @@
2324 },
2425 },
2526 "packages": {
27+ "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
28+
29+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
30+
31+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
32+
33+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
34+
35+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
36+
37+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
38+
39+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
40+
41+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
42+
43+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
44+
2645 "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
2746
2847 "@chneau/elysia-compression": ["@chneau/elysia-compression@1.0.11", "", { "dependencies": { "elysia": "^1.1.9" } }, "sha512-J4wfz5Qs68p/DNvkg5DXxo8sxJKSibzaUBP4W56uvqgs9NtHG0LC6tnyv7sAPEjNKzh21l6kinMoXbtVDp/KSQ=="],
Mpackage.json
@@ -1,26 +1,29 @@
11 {
2- "name": "zbin",
3- "version": "1.0.0",
4- "scripts": {
5- "build": "bun build src/client*.ts --outdir assets/dist",
6- "dev": "bun run build && bun run --watch src/index.ts",
7- "prod": "bun run build && bun run src/index.ts"
8- },
9- "dependencies": {
10- "@chneau/elysia-compression": "^1.0.11",
11- "@elysiajs/cron": "^1.2.0",
12- "@elysiajs/html": "^1.2.0",
13- "@elysiajs/static": "^1.2.0",
14- "elysia": "^1.2.0",
15- "file-type": "^20.0.1",
16- "highlight.js": "^11.11.1",
17- "pkcs7-padding": "^0.1.1",
18- "utf-8-validate": "^6.0.5"
19- },
20- "devDependencies": {
21- "@types/pkcs7-padding": "^0.1.3",
22- "@types/utf-8-validate": "^5.0.2",
23- "bun-types": "latest"
24- },
25- "module": "src/index.js"
2+ "name": "zbin",
3+ "version": "1.0.0",
4+ "scripts": {
5+ "build": "bun build src/client*.ts --outdir assets/dist",
6+ "dev": "bun run build && bun run --watch src/index.ts",
7+ "prod": "bun run build && bun run src/index.ts",
8+ "format": "biome format --write",
9+ "lint": "biome lint"
10+ },
11+ "dependencies": {
12+ "@chneau/elysia-compression": "^1.0.11",
13+ "@elysiajs/cron": "^1.2.0",
14+ "@elysiajs/html": "^1.2.0",
15+ "@elysiajs/static": "^1.2.0",
16+ "elysia": "^1.2.0",
17+ "file-type": "^20.0.1",
18+ "highlight.js": "^11.11.1",
19+ "pkcs7-padding": "^0.1.1",
20+ "utf-8-validate": "^6.0.5"
21+ },
22+ "devDependencies": {
23+ "@biomejs/biome": "2.4.16",
24+ "@types/pkcs7-padding": "^0.1.3",
25+ "@types/utf-8-validate": "^5.0.2",
26+ "bun-types": "latest"
27+ },
28+ "module": "src/index.js"
2629 }
Msrc/client-index.ts
@@ -1,78 +1,102 @@
1-async function encrypt(content: Uint8Array, password: string): Promise<ArrayBuffer> {
2- const iv = crypto.getRandomValues(new Uint8Array(16))
3- const salt = crypto.getRandomValues(new Uint8Array(16))
4- const keyMaterial = await crypto.subtle.importKey(
5- "raw",
6- new TextEncoder().encode(password),
7- { name: "PBKDF2" },
8- false,
9- ["deriveBits", "deriveKey"]
10- )
11- const key = await crypto.subtle.deriveKey(
12- {
13- name: "PBKDF2",
14- salt: salt,
15- iterations: 100000,
16- hash: "SHA-512"
17- },
18- keyMaterial,
19- { name: "AES-CBC", length: 256 },
20- false,
21- ["encrypt"]
22- )
23- const encryptedContent = await crypto.subtle.encrypt(
24- { name: "AES-CBC", iv: iv },
25- key,
26- content
27- )
28- return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)]).buffer
1+async function encrypt(
2+ content: Uint8Array<ArrayBuffer>,
3+ password: string,
4+): Promise<ArrayBuffer> {
5+ const iv = crypto.getRandomValues(new Uint8Array(16));
6+ const salt = crypto.getRandomValues(new Uint8Array(16));
7+ const keyMaterial = await crypto.subtle.importKey(
8+ "raw",
9+ new TextEncoder().encode(password),
10+ { name: "PBKDF2" },
11+ false,
12+ ["deriveBits", "deriveKey"],
13+ );
14+ const key = await crypto.subtle.deriveKey(
15+ {
16+ name: "PBKDF2",
17+ salt: salt,
18+ iterations: 100000,
19+ hash: "SHA-512",
20+ },
21+ keyMaterial,
22+ { name: "AES-CBC", length: 256 },
23+ false,
24+ ["encrypt"],
25+ );
26+ const encryptedContent = await crypto.subtle.encrypt(
27+ { name: "AES-CBC", iv: iv },
28+ key,
29+ content,
30+ );
31+ return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)])
32+ .buffer;
2933 }
3034
3135 async function uploadFile() {
32- (document.getElementById("upload-dialog") as HTMLDialogElement | null)?.showModal()
33- const passwordInput = document.getElementById("password") as HTMLInputElement | null
34- const fileInput = document.getElementById("file") as HTMLInputElement | null
35- const filenameInput = document.getElementById("filename") as HTMLInputElement | null
36- const encryptedInput = document.getElementById("encrypted") as HTMLInputElement | null
37- const form = document.getElementById("uploadForm") as HTMLFormElement | null
36+ (
37+ document.getElementById("upload-dialog") as HTMLDialogElement | null
38+ )?.showModal();
39+ const passwordInput = document.getElementById(
40+ "password",
41+ ) as HTMLInputElement | null;
42+ const fileInput = document.getElementById("file") as HTMLInputElement | null;
43+ const filenameInput = document.getElementById(
44+ "filename",
45+ ) as HTMLInputElement | null;
46+ const encryptedInput = document.getElementById(
47+ "encrypted",
48+ ) as HTMLInputElement | null;
49+ const form = document.getElementById("uploadForm") as HTMLFormElement | null;
3850
39- if (!passwordInput || !fileInput || !filenameInput || !encryptedInput || !form) {
40- console.error("Missing required elements.")
41- console.error(passwordInput, fileInput, filenameInput, encryptedInput, form)
42- return
43- }
51+ if (
52+ !passwordInput ||
53+ !fileInput ||
54+ !filenameInput ||
55+ !encryptedInput ||
56+ !form
57+ ) {
58+ console.error("Missing required elements.");
59+ console.error(
60+ passwordInput,
61+ fileInput,
62+ filenameInput,
63+ encryptedInput,
64+ form,
65+ );
66+ return;
67+ }
4468
45- const password = passwordInput.value;
46- if (!password) {
47- form.submit()
48- return
49- }
69+ const password = passwordInput.value;
70+ if (!password) {
71+ form.submit();
72+ return;
73+ }
5074
51- const file = fileInput.files?.[0];
52- if (!file) {
53- return
54- }
75+ const file = fileInput.files?.[0];
76+ if (!file) {
77+ return;
78+ }
5579
56- const content = await file.arrayBuffer()
57- const encryptedContent = await encrypt(new Uint8Array(content), password)
80+ const content = await file.arrayBuffer();
81+ const encryptedContent = await encrypt(new Uint8Array(content), password);
5882
59- const myFile = new File([encryptedContent], file.name)
60- const dataTransfer = new DataTransfer()
61- dataTransfer.items.add(myFile)
83+ const myFile = new File([encryptedContent], file.name);
84+ const dataTransfer = new DataTransfer();
85+ dataTransfer.items.add(myFile);
6286
63- fileInput.files = dataTransfer.files
64- encryptedInput.checked = true
65- console.log("encryption done")
66- form.submit()
87+ fileInput.files = dataTransfer.files;
88+ encryptedInput.checked = true;
89+ console.log("encryption done");
90+ form.submit();
6791 }
6892
69-const jsguards = document.getElementsByClassName("jsguard")
93+const jsguards = document.getElementsByClassName("jsguard");
7094 for (let i = 0; i < jsguards.length; i++) {
71- jsguards[i].remove()
95+ jsguards[i].remove();
7296 }
7397
7498 (document.getElementById("uploadForm") as HTMLFormElement | null)?.reset();
75-window.uploadFile = uploadFile
99+window.uploadFile = uploadFile;
76100 //disable bfcache, otherwise dialog will stay open when navigating back
77-window.addEventListener('unload', function () { });
78-window.addEventListener('beforeunload', function () { });
101+window.addEventListener("unload", () => {});
102+window.addEventListener("beforeunload", () => {});
Msrc/client-show.ts
@@ -1,139 +1,158 @@
1-import { fileTypeFromBuffer } from 'file-type'
2-import hljs from 'highlight.js'
3-import { humanFileSize } from './shared'
1+import { fileTypeFromBuffer } from "file-type";
2+import hljs from "highlight.js";
3+import { humanFileSize } from "./shared";
44
5-async function decrypt(encryptedContent: ArrayBuffer, password: string): Promise<ArrayBuffer> {
6- const encryptedArray = new Uint8Array(encryptedContent)
7- const salt = encryptedArray.slice(0, 16)
8- const iv = encryptedArray.slice(16, 32)
9- const keyMaterial = await crypto.subtle.importKey(
10- "raw",
11- new TextEncoder().encode(password),
12- { name: "PBKDF2" },
13- false,
14- ["deriveBits", "deriveKey"]
15- )
16- const key = await crypto.subtle.deriveKey(
17- {
18- name: "PBKDF2",
19- salt: salt,
20- iterations: 100000,
21- hash: "SHA-512"
22- },
23- keyMaterial,
24- { name: "AES-CBC", length: 256 },
25- false,
26- ["decrypt"]
27- )
28- const decryptedContent = await crypto.subtle.decrypt(
29- { name: "AES-CBC", iv: iv },
30- key,
31- encryptedArray.slice(32)
32- )
33- return decryptedContent
5+async function decrypt(
6+ encryptedContent: ArrayBuffer,
7+ password: string,
8+): Promise<ArrayBuffer> {
9+ const encryptedArray = new Uint8Array(encryptedContent);
10+ const salt = encryptedArray.slice(0, 16);
11+ const iv = encryptedArray.slice(16, 32);
12+ const keyMaterial = await crypto.subtle.importKey(
13+ "raw",
14+ new TextEncoder().encode(password),
15+ { name: "PBKDF2" },
16+ false,
17+ ["deriveBits", "deriveKey"],
18+ );
19+ const key = await crypto.subtle.deriveKey(
20+ {
21+ name: "PBKDF2",
22+ salt: salt,
23+ iterations: 100000,
24+ hash: "SHA-512",
25+ },
26+ keyMaterial,
27+ { name: "AES-CBC", length: 256 },
28+ false,
29+ ["decrypt"],
30+ );
31+ const decryptedContent = await crypto.subtle.decrypt(
32+ { name: "AES-CBC", iv: iv },
33+ key,
34+ encryptedArray.slice(32),
35+ );
36+ return decryptedContent;
3437 }
3538
3639 function isValidUTF8(buf: ArrayBuffer) {
37- try {
38- new TextDecoder("utf-8", { fatal: true }).decode(buf)
39- return true
40- } catch (e) {
41- return false
42- }
40+ try {
41+ new TextDecoder("utf-8", { fatal: true }).decode(buf);
42+ return true;
43+ } catch (_e) {
44+ return false;
45+ }
4346 }
4447
4548 async function showContent(content: ArrayBuffer, filetype: string) {
46- const filesize = document.getElementById("filesize") as HTMLDivElement | null
47- if (filesize) {
48- filesize.textContent = `size: ${humanFileSize(content.byteLength)}`
49- }
49+ const filesize = document.getElementById("filesize") as HTMLDivElement | null;
50+ if (filesize) {
51+ filesize.textContent = `size: ${humanFileSize(content.byteLength)}`;
52+ }
5053
51- const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null
52- if (!mediaBox) {
53- console.error("Missing mediaBox element.")
54- return
55- }
56- let preview: HTMLElement = document.createElement("div")
57- preview.textContent = "This file can't be previewed"
58- if (isValidUTF8(content) && filetype !== "blob") {
59- preview = document.createElement("pre")
60- if (filetype === "none") {
61- preview.textContent = new TextDecoder().decode(content)
62- } else {
63- preview.innerHTML = hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value
64- }
65- } else {
66- const filetype = await fileTypeFromBuffer(content)
67- if (filetype) {
68- const blob = new Blob([content], { type: filetype.mime })
69- const url = URL.createObjectURL(blob)
70- if (filetype.mime.startsWith("audio/")) {
71- preview = document.createElement("audio")
72- preview.setAttribute("controls", "")
73- preview.setAttribute("src", url)
74- } else if (filetype.mime.startsWith("video/")) {
75- preview = document.createElement("video")
76- preview.setAttribute("controls", "")
77- preview.setAttribute("src", url)
78- } else if (filetype.mime.startsWith("image/")) {
79- preview = document.createElement("img")
80- preview.setAttribute("src", url)
81- }
82- }
83- }
84- mediaBox.innerHTML = ""
85- mediaBox.appendChild(preview)
54+ const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null;
55+ if (!mediaBox) {
56+ console.error("Missing mediaBox element.");
57+ return;
58+ }
59+ let preview: HTMLElement = document.createElement("div");
60+ preview.textContent = "This file can't be previewed";
61+ if (isValidUTF8(content) && filetype !== "blob") {
62+ preview = document.createElement("pre");
63+ if (filetype === "none") {
64+ preview.textContent = new TextDecoder().decode(content);
65+ } else {
66+ preview.innerHTML = hljs.highlight(
67+ new TextDecoder("utf-8").decode(content),
68+ { language: filetype },
69+ ).value;
70+ }
71+ } else {
72+ const filetype = await fileTypeFromBuffer(content);
73+ if (filetype) {
74+ const blob = new Blob([content], { type: filetype.mime });
75+ const url = URL.createObjectURL(blob);
76+ if (filetype.mime.startsWith("audio/")) {
77+ preview = document.createElement("audio");
78+ preview.setAttribute("controls", "");
79+ preview.setAttribute("src", url);
80+ } else if (filetype.mime.startsWith("video/")) {
81+ preview = document.createElement("video");
82+ preview.setAttribute("controls", "");
83+ preview.setAttribute("src", url);
84+ } else if (filetype.mime.startsWith("image/")) {
85+ preview = document.createElement("img");
86+ preview.setAttribute("src", url);
87+ }
88+ }
89+ }
90+ mediaBox.innerHTML = "";
91+ mediaBox.appendChild(preview);
8692 }
8793
88-let content: ArrayBuffer
89-let filetype: string
90-let filename: string
94+let content: ArrayBuffer;
95+let filetype: string;
96+let filename: string;
9197
9298 async function onPasswordSubmit() {
93- const passwordInput = document.getElementById("password") as HTMLInputElement | null
94- const passwordLabel = document.getElementById("password-label") as HTMLLabelElement | null
95- if (!passwordInput || !passwordLabel) {
96- console.error("Missing passwordInput or passwordLabel element.")
97- return
98- }
99- try {
100- content = await decrypt(content, passwordInput.value)
101- } catch (e) {
102- passwordLabel.textContent = "Incorrect password"
103- return
104- }
105- const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null
106- dialog?.close()
107- const downloadForm = document.getElementById("download-form") as HTMLFormElement | null
108- downloadForm?.setAttribute("action", "")
109- downloadForm?.addEventListener("submit", (e) => {
110- e.preventDefault()
111- const blob = new Blob([content])
112- const url = URL.createObjectURL(blob)
113- const link = document.createElement("a")
114- link.download = filename
115- link.href = url
116- link.click();
117- })
118- showContent(content, filetype)
99+ const passwordInput = document.getElementById(
100+ "password",
101+ ) as HTMLInputElement | null;
102+ const passwordLabel = document.getElementById(
103+ "password-label",
104+ ) as HTMLLabelElement | null;
105+ if (!passwordInput || !passwordLabel) {
106+ console.error("Missing passwordInput or passwordLabel element.");
107+ return;
108+ }
109+ try {
110+ content = await decrypt(content, passwordInput.value);
111+ } catch (_e) {
112+ passwordLabel.textContent = "Incorrect password";
113+ return;
114+ }
115+ const dialog = document.getElementById(
116+ "password-dialog",
117+ ) as HTMLDialogElement | null;
118+ dialog?.close();
119+ const downloadForm = document.getElementById(
120+ "download-form",
121+ ) as HTMLFormElement | null;
122+ downloadForm?.setAttribute("action", "");
123+ downloadForm?.addEventListener("submit", (e) => {
124+ e.preventDefault();
125+ const blob = new Blob([content]);
126+ const url = URL.createObjectURL(blob);
127+ const link = document.createElement("a");
128+ link.download = filename;
129+ link.href = url;
130+ link.click();
131+ });
132+ showContent(content, filetype);
119133 }
120134
121-const uuid = window.location.pathname.split("/").filter((x) => x !== "").reverse()[0]
135+const uuid = window.location.pathname
136+ .split("/")
137+ .filter((x) => x !== "")
138+ .reverse()[0];
122139
123-fetch("/raw/" + uuid + "?ignore_password=true").then(async (response) => {
124- content = await response.arrayBuffer()
125- const mediaBox = document.getElementById("mediabox")
126- if(mediaBox) {
127- mediaBox.innerHTML = ""
128- }
129- filetype = response.headers.get("filetype") || "none"
130- filename = response.headers.get("filename") || ""
131- if (response.headers.get("encrypted") === "true") {
132- const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null
133- dialog?.showModal()
134- } else {
135- showContent(content, filetype)
136- }
140+fetch(`/raw/${uuid}?ignore_password=true`).then(async (response) => {
141+ content = await response.arrayBuffer();
142+ const mediaBox = document.getElementById("mediabox");
143+ if (mediaBox) {
144+ mediaBox.innerHTML = "";
145+ }
146+ filetype = response.headers.get("filetype") || "none";
147+ filename = response.headers.get("filename") || "";
148+ if (response.headers.get("encrypted") === "true") {
149+ const dialog = document.getElementById(
150+ "password-dialog",
151+ ) as HTMLDialogElement | null;
152+ dialog?.showModal();
153+ } else {
154+ showContent(content, filetype);
155+ }
137156 });
138157
139-(window as any).onPasswordSubmit = onPasswordSubmit
158+Object.assign(window, { onPasswordSubmit });
Msrc/components.tsx
@@ -1,227 +1,307 @@
1-import { Html } from '@elysiajs/html'
2-import { PropsWithChildren } from '@kitajs/html'
3-import { escapeHTML } from 'bun'
4-import { fileTypeFromBuffer } from 'file-type'
5-import hljs from 'highlight.js'
6-import isValidUTF8 from 'utf-8-validate'
7-import { humanFileSize } from './shared'
1+// biome-ignore lint/correctness/noUnusedImports: Html is the JSX factory (tsconfig jsxFactory: Html.createElement)
2+import { Html } from "@elysiajs/html";
3+import type { PropsWithChildren } from "@kitajs/html";
4+import { escapeHTML } from "bun";
5+import { fileTypeFromBuffer } from "file-type";
6+import hljs from "highlight.js";
7+import isValidUTF8 from "utf-8-validate";
8+import { humanFileSize } from "./shared";
89
9-export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort())
10+export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort());
1011
1112 function Template(props: PropsWithChildren<{ css: string }>) {
12- return (
13- <>
14- {'<!doctype html>'}
15- <html lang="en">
16- <head>
17- <title>⚡ZBin⚡</title>
18- <meta charset="UTF-8" />
19- <meta name="viewport" content="width=device-width, initial-scale=1" />
20- <link rel="stylesheet" href={props.css} />
21- <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>" />
22- </head>
23- <body>{props.children}</body>
24- </html>
25- </>
26- )
13+ return (
14+ <>
15+ {"<!doctype html>"}
16+ <html lang="en">
17+ <head>
18+ <title>⚡ZBin⚡</title>
19+ <meta charset="UTF-8" />
20+ <meta name="viewport" content="width=device-width, initial-scale=1" />
21+ <link rel="stylesheet" href={props.css} />
22+ <link
23+ rel="icon"
24+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>"
25+ />
26+ </head>
27+ <body>{props.children}</body>
28+ </html>
29+ </>
30+ );
2731 }
2832
2933 export function NotFound() {
30- return (
31- <Template css="/default.css">
32- <h1>404</h1>
33- <p>File not found</p>
34- </Template>
35- )
34+ return (
35+ <Template css="/default.css">
36+ <h1>404</h1>
37+ <p>File not found</p>
38+ </Template>
39+ );
3640 }
3741
3842 export function Index(hostname: string, withJs: boolean) {
39- let body = (
40- <>
41- <dialog id="upload-dialog">
42- File is being uploaded, please wait
43- </dialog>
44- {withJs ?
45- <div style="text-align: center">JS version with client-side encryption, for the version without js an with server-side encryption, go <a href="/">here</a>.</div>
46- : <div style="text-align: center">JS-less version with server-side encryption, for the version with client-side encryption, go to <a href="/js">/js</a>.</div>
47- }
48- <hr />
49- <form action={withJs ? "/upload?withJs=true" : "/upload"} id="uploadForm" method="post" enctype="multipart/form-data" onsubmit={withJs ? "uploadFile(); return false" : undefined}>
50- <input type="reset" value="Reset form" />
51- <input required={true} type="file" id="file" name="file" />
52- <label for="filename">File name override:</label>
53- <small>Optional. If empty, will use the uploaded file name.</small>
54- <input type="text" id="filename" name="filename" placeholder="File name" />
55- <label for="filetype">File type:</label>
56- <small>Used for syntax highlighting, if the file is only containing valid UTF-8 characters. Otherwise it will always be stored as a binary blob. Can select <code>blob</code> to force this detection.</small>
57- <select size="10" required={true} id="filetype" name="filetype">
58- {
59- filetypes.map((filetype, i) => (
60- <option selected={i === 0} value={filetype}>{filetype}</option>
61- ))
62- }
63- </select>
64- <label for="delete-in">Delete in:</label>
65- <small>Optional. If set, file will be deleted after the given number of minutes.</small>
66- <input type="number" id="delete-in" name="delete_in_minutes" min="0" placeholder="0" />
67- <label for="password">Password:</label>
68- <small>Optional. If set, file will be encrypted and requires the given password to open.</small>
69- <input type="password" id="password" name="password" placeholder="Password" />
70- <div>
71- <label for="encrypted">Already encrypted:</label>
72- <input type="checkbox" id="encrypted" name="encrypted" />
73- </div>
74- <small>If set, file is already encrypted with an algorithm like in the python file available below. Password is ignored in this case.</small>
75- <input type="submit" value="Upload File" />
76- <hr />
77- <h3>curl guide</h3>
78- You can use curl to upload and download files<br />
79- Upload:
80- <pre>curl {hostname}upload \{"\n"}
81- -F file=@/path/to/file \{"\n"}
82- -F filetype="plaintext" \{"\n"}
83- # optional{"\n"}
84- -F filename="file name" \{"\n"}
85- # optional{"\n"}
86- -F delete_in_minutes="60" \{"\n"}
87- # optional{"\n"}
88- -F password="mypassword" \{"\n"}
89- # optional{"\n"}
90- -F encrypted="on"</pre>
91- <p>
92- If you upload an already encrypted file, you should set the <code>encrypted="on"</code> parameter and encrypt it in a compatible way, so the frontend/backend can also decrypt it.
93- You can use this python script to encrypt a file locally: <a href="/encrypt.py">encrypt.py</a>
94- </p>
95- <br />
96- Download:
97- <pre>curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}
98- # optional, if encrypted with password{"\n"}
99- --cookie "password=mypassword"</pre>
100- <p>
101- If ignore_password is set to true, then encrypted files can be downloaded directly (in encrypted form) without supplying the password.
102- </p>
103- </form>
104- </>
105- )
106- if (withJs) {
107- body = (
108- <>
109- <div class="jsguard">JS seems to be disabled, enable it and refesh the page, or go back to<a href="/">here</a> for the JS-less version.</div>
110- <div class="jsguard">
111- {body}
112- </div>
113- <script src="/dist/client-index.js" />
114- </>
115- )
116- }
117- return (
118- <Template css="/default.css">
119- <h1>⚡ZBin⚡</h1>
120- {body}
121- </Template>
122- )
43+ let body = (
44+ <>
45+ <dialog id="upload-dialog">File is being uploaded, please wait</dialog>
46+ {withJs ? (
47+ <div style="text-align: center">
48+ JS version with client-side encryption, for the version without js an
49+ with server-side encryption, go <a href="/">here</a>.
50+ </div>
51+ ) : (
52+ <div style="text-align: center">
53+ JS-less version with server-side encryption, for the version with
54+ client-side encryption, go to <a href="/js">/js</a>.
55+ </div>
56+ )}
57+ <hr />
58+ <form
59+ action={withJs ? "/upload?withJs=true" : "/upload"}
60+ id="uploadForm"
61+ method="post"
62+ enctype="multipart/form-data"
63+ onsubmit={withJs ? "uploadFile(); return false" : undefined}
64+ >
65+ <input type="reset" value="Reset form" />
66+ <input required={true} type="file" id="file" name="file" />
67+ <label for="filename">File name override:</label>
68+ <small>Optional. If empty, will use the uploaded file name.</small>
69+ <input
70+ type="text"
71+ id="filename"
72+ name="filename"
73+ placeholder="File name"
74+ />
75+ <label for="filetype">File type:</label>
76+ <small>
77+ Used for syntax highlighting, if the file is only containing valid
78+ UTF-8 characters. Otherwise it will always be stored as a binary blob.
79+ Can select <code>blob</code> to force this detection.
80+ </small>
81+ <select size="10" required={true} id="filetype" name="filetype">
82+ {filetypes.map((filetype, i) => (
83+ <option selected={i === 0} value={filetype}>
84+ {filetype}
85+ </option>
86+ ))}
87+ </select>
88+ <label for="delete-in">Delete in:</label>
89+ <small>
90+ Optional. If set, file will be deleted after the given number of
91+ minutes.
92+ </small>
93+ <input
94+ type="number"
95+ id="delete-in"
96+ name="delete_in_minutes"
97+ min="0"
98+ placeholder="0"
99+ />
100+ <label for="password">Password:</label>
101+ <small>
102+ Optional. If set, file will be encrypted and requires the given
103+ password to open.
104+ </small>
105+ <input
106+ type="password"
107+ id="password"
108+ name="password"
109+ placeholder="Password"
110+ />
111+ <div>
112+ <label for="encrypted">Already encrypted:</label>
113+ <input type="checkbox" id="encrypted" name="encrypted" />
114+ </div>
115+ <small>
116+ If set, file is already encrypted with an algorithm like in the python
117+ file available below. Password is ignored in this case.
118+ </small>
119+ <input type="submit" value="Upload File" />
120+ <hr />
121+ <h3>curl guide</h3>
122+ You can use curl to upload and download files
123+ <br />
124+ Upload:
125+ <pre>
126+ curl {hostname}upload \{"\n"}
127+ -F file=@/path/to/file \{"\n"}
128+ -F filetype="plaintext" \{"\n"}# optional{"\n"}
129+ -F filename="file name" \{"\n"}# optional{"\n"}
130+ -F delete_in_minutes="60" \{"\n"}# optional{"\n"}
131+ -F password="mypassword" \{"\n"}# optional{"\n"}
132+ -F encrypted="on"
133+ </pre>
134+ <p>
135+ If you upload an already encrypted file, you should set the{" "}
136+ <code>encrypted="on"</code> parameter and encrypt it in a compatible
137+ way, so the frontend/backend can also decrypt it. You can use this
138+ python script to encrypt a file locally:{" "}
139+ <a href="/encrypt.py">encrypt.py</a>
140+ </p>
141+ <br />
142+ Download:
143+ <pre>
144+ curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}# optional,
145+ if encrypted with password{"\n"}
146+ --cookie "password=mypassword"
147+ </pre>
148+ <p>
149+ If ignore_password is set to true, then encrypted files can be
150+ downloaded directly (in encrypted form) without supplying the
151+ password.
152+ </p>
153+ </form>
154+ </>
155+ );
156+ if (withJs) {
157+ body = (
158+ <>
159+ <div class="jsguard">
160+ JS seems to be disabled, enable it and refesh the page, or go back to
161+ <a href="/">here</a> for the JS-less version.
162+ </div>
163+ <div class="jsguard">{body}</div>
164+ <script src="/dist/client-index.js" />
165+ </>
166+ );
167+ }
168+ return (
169+ <Template css="/default.css">
170+ <h1>⚡ZBin⚡</h1>
171+ {body}
172+ </Template>
173+ );
123174 }
124175
125-
126176 export function DecryptFile(filename: string, uuid: string) {
127- return (
128- <Template css="/default.css">
129- <h1>Encrypted file: {filename}</h1>
130- <form action={`/set-cookie/${uuid}`} method="post">
131- <label for="password">Enter a Password to decrypt the file</label>
132- <input type="password" name="password" />
133- <input type="submit" value="Submit" />
134- </form>
135- </Template>
136- )
177+ return (
178+ <Template css="/default.css">
179+ <h1>Encrypted file: {filename}</h1>
180+ <form action={`/set-cookie/${uuid}`} method="post">
181+ <label for="password">Enter a Password to decrypt the file</label>
182+ <input type="password" name="password" />
183+ <input type="submit" value="Submit" />
184+ </form>
185+ </Template>
186+ );
137187 }
138188
139189 export function WrongPassword() {
140- return (
141- <Template css="/default.css">
142- <h1>Incorrect password</h1>
143- <p>Reload page to try again</p>
144- </Template>
145- )
190+ return (
191+ <Template css="/default.css">
192+ <h1>Incorrect password</h1>
193+ <p>Reload page to try again</p>
194+ </Template>
195+ );
146196 }
147197
148198 function humanReadableTime(minutes: number) {
149- const years = Math.floor(minutes / 525600);
150- minutes %= 525600;
151- const days = Math.floor(minutes / 1440);
152- minutes %= 1440;
153- const hours = Math.floor(minutes / 60);
154- minutes %= 60;
199+ const years = Math.floor(minutes / 525600);
200+ minutes %= 525600;
201+ const days = Math.floor(minutes / 1440);
202+ minutes %= 1440;
203+ const hours = Math.floor(minutes / 60);
204+ minutes %= 60;
155205
156- let result = "";
157- if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `
158- if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `
159- if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `
160- if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `
161- if (result === "") result = "0 minutes"
162- return result.trim();
206+ let result = "";
207+ if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `;
208+ if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `;
209+ if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `;
210+ if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `;
211+ if (result === "") result = "0 minutes";
212+ return result.trim();
163213 }
164214
165-export async function ShowFile(filename: string, uuid: string, content: Uint8Array | null, filetype: string, delete_at: number | null) {
166- //if content is null, this is for the js frontend
167- let preview;
168- if (!content) {
169- preview = <>Please wait for the file to load</>
170- } else {
171- preview = <>This file can't be previewed</>
172- if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") {
173- if (filetype === "none") {
174- preview = <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre>
175- } else {
176- preview = <pre>{hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value}</pre>
177- }
178- } else {
179- const filetype = await fileTypeFromBuffer(content)
180- if (filetype) {
181- const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`;
182- if (filetype.mime.startsWith("audio/")) {
183- preview = <audio controls="" src={base64Content} />
184- } else if (filetype.mime.startsWith("video/")) {
185- preview = <video controls src={base64Content} />
186- } else if (filetype.mime.startsWith("image/")) {
187- preview = <img src={base64Content} />
188- }
189- }
190- }
191- }
215+export async function ShowFile(
216+ filename: string,
217+ uuid: string,
218+ content: Uint8Array | null,
219+ filetype: string,
220+ delete_at: number | null,
221+) {
222+ //if content is null, this is for the js frontend
223+ let preview: JSX.Element;
224+ if (!content) {
225+ preview = <>Please wait for the file to load</>;
226+ } else {
227+ preview = <>This file can't be previewed</>;
228+ if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") {
229+ if (filetype === "none") {
230+ preview = (
231+ <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre>
232+ );
233+ } else {
234+ preview = (
235+ <pre>
236+ {
237+ hljs.highlight(new TextDecoder("utf-8").decode(content), {
238+ language: filetype,
239+ }).value
240+ }
241+ </pre>
242+ );
243+ }
244+ } else {
245+ const filetype = await fileTypeFromBuffer(content);
246+ if (filetype) {
247+ const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`;
248+ if (filetype.mime.startsWith("audio/")) {
249+ preview = <audio controls="" src={base64Content} />;
250+ } else if (filetype.mime.startsWith("video/")) {
251+ preview = <video controls src={base64Content} />;
252+ } else if (filetype.mime.startsWith("image/")) {
253+ preview = <img src={base64Content} alt={filename} />;
254+ }
255+ }
256+ }
257+ }
192258
193- return (
194- <Template css="/show.css">
195- <dialog id="password-dialog">
196- <form onsubmit="onPasswordSubmit(); return false">
197- <label id="password-label" for="password">File is encrypted, enter password to decrypt:</label>
198- <input required type="password" id="password" />
199- <input type="submit" value="Submit" />
200- </form>
201- </dialog>
202- <div id="content">
203- <div id="filename">{filename}</div>
204- <div id="mediabox">{preview}</div>
205- </div>
206- <div id="sidebar">
207- <h3>File info</h3>
208- <hr />
209- <p id="filesize">size: {content ? humanFileSize(content.byteLength) : "unknown"}</p>
210- <p>declared type: {filetype}</p>
211- {delete_at ? <p>delete at: {new Date(delete_at * 1000).toISOString()} (in {humanReadableTime(Math.floor((delete_at - (Date.now() / 1000)) / 60))})</p> : ""}
212- <form id="download-form" action={`/raw/${uuid}`} method="get">
213- <button>Download</button>
214- </form>
215- </div>
216- {content ? "" : <script src="/dist/client-show.js" />}
217- </Template>
218- )
259+ return (
260+ <Template css="/show.css">
261+ <dialog id="password-dialog">
262+ <form onsubmit="onPasswordSubmit(); return false">
263+ <label id="password-label" for="password">
264+ File is encrypted, enter password to decrypt:
265+ </label>
266+ <input required type="password" id="password" />
267+ <input type="submit" value="Submit" />
268+ </form>
269+ </dialog>
270+ <div id="content">
271+ <div id="filename">{filename}</div>
272+ <div id="mediabox">{preview}</div>
273+ </div>
274+ <div id="sidebar">
275+ <h3>File info</h3>
276+ <hr />
277+ <p id="filesize">
278+ size: {content ? humanFileSize(content.byteLength) : "unknown"}
279+ </p>
280+ <p>declared type: {filetype}</p>
281+ {delete_at ? (
282+ <p>
283+ delete at: {new Date(delete_at * 1000).toISOString()} (in{" "}
284+ {humanReadableTime(
285+ Math.floor((delete_at - Date.now() / 1000) / 60),
286+ )}
287+ )
288+ </p>
289+ ) : (
290+ ""
291+ )}
292+ <form id="download-form" action={`/raw/${uuid}`} method="get">
293+ <button type="submit">Download</button>
294+ </form>
295+ </div>
296+ {content ? "" : <script src="/dist/client-show.js" />}
297+ </Template>
298+ );
219299 }
220300
221301 export function SetCookie(uuid: string) {
222- return (
223- <Template css="/default.css">
224- Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
225- </Template>
226- )
302+ return (
303+ <Template css="/default.css">
304+ Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
305+ </Template>
306+ );
227307 }
Msrc/index.ts
@@ -1,174 +1,265 @@
1-import { Elysia, StatusMap, t } from "elysia"
2-import { Database } from "bun:sqlite"
3-import { randomUUIDv7 } from "bun"
4-import staticPlugin from "@elysiajs/static"
5-import { html } from '@elysiajs/html'
6-import crypto from 'crypto'
7-import { DecryptFile, filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components"
8-import cron from "@elysiajs/cron"
9-
10-const db = new Database("./db/db.sqlite")
11-db.exec("PRAGMA foreign_keys = ON")
12-db.exec("PRAGMA journal_mode = WAL2")
13-db.exec("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")
14-db.exec("PRAGMA optimize")
1+import { Database } from "bun:sqlite";
2+import crypto from "node:crypto";
3+import cron from "@elysiajs/cron";
4+import { html } from "@elysiajs/html";
5+import staticPlugin from "@elysiajs/static";
6+import { randomUUIDv7 } from "bun";
7+import { Elysia, StatusMap, t } from "elysia";
8+import {
9+ DecryptFile,
10+ filetypes,
11+ Index,
12+ NotFound,
13+ SetCookie,
14+ ShowFile,
15+ WrongPassword,
16+} from "./components";
1517
18+const db = new Database("./db/db.sqlite");
19+db.run("PRAGMA foreign_keys = ON");
20+db.run("PRAGMA journal_mode = WAL2");
21+db.run(
22+ "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",
23+);
24+db.run("PRAGMA optimize");
1625
1726 function encrypt(content: Uint8Array, password: string): Buffer<ArrayBuffer> {
18- let iv = crypto.randomBytes(16)
19- const salt = crypto.randomBytes(16)
20- const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
21- let cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
22- let encrypted_content = Buffer.concat([salt, iv, cipher.update(content), cipher.final()])
23- return encrypted_content
27+ const iv = crypto.randomBytes(16);
28+ const salt = crypto.randomBytes(16);
29+ const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512");
30+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
31+ return Buffer.concat([salt, iv, cipher.update(content), cipher.final()]);
2432 }
2533
2634 function decrypt(encrypted_content: Uint8Array, password: string): Buffer {
27- const salt = encrypted_content.slice(0, 16)
28- const iv = encrypted_content.slice(16, 32)
29- const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
30- let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
31- let content = Buffer.concat([decipher.update(encrypted_content.slice(32)), decipher.final()])
32- return content
35+ const salt = encrypted_content.slice(0, 16);
36+ const iv = encrypted_content.slice(16, 32);
37+ const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512");
38+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
39+ return Buffer.concat([
40+ decipher.update(encrypted_content.slice(32)),
41+ decipher.final(),
42+ ]);
3343 }
3444
3545 function stringArrayToEnum<T extends string>(
36- arr: readonly T[]
46+ arr: readonly T[],
3747 ): { [K in T]: K } {
38- return arr.reduce((acc, key) => {
39- acc[key] = key;
40- return acc;
41- }, Object.create(null));
48+ return arr.reduce((acc, key) => {
49+ acc[key] = key;
50+ return acc;
51+ }, Object.create(null));
4252 }
4353
4454 const app = new Elysia({
45- serve: {
46- maxRequestBodySize: 1024 * 1024 * 1024 // 1GB
47- }
55+ serve: {
56+ maxRequestBodySize: 1024 * 1024 * 1024, // 1GB
57+ },
4858 })
49- .use(staticPlugin({ assets: "./assets", prefix: "/" }))
50- .use(html())
51- .use(cron({
52- name: "delete",
53- pattern: "*/5 * * * * *",
54- run() {
55- db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')")
56- }
57- }))
58- .get("/", ({ server }) => Index(server?.url.toString()!, false))
59- .get("/js", ({ server }) => Index(server?.url.toString()!, true))
60- .post("/upload", async ({ set, body, query }) => {
61-
62- const uuid = randomUUIDv7()
63- let content = Buffer.from(await body.file.bytes())
64- let encrypted = false;
65- let delete_at: number | null = null
66- if (body.delete_in_minutes) {
67- delete_at = Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60
68- }
69- if (body.encrypted === "on") {
70- encrypted = true
71- } else if (body.password) {
72- content = encrypt(content, body.password)
73- encrypted = true
74- }
75- db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
76- [uuid, body.filename || body.file.name, content, body.filetype, encrypted, delete_at])
77- set.status = StatusMap["See Other"]
78- if (query.withJs) {
79- set.headers["location"] = `/show-js/${uuid}`
80- } else {
81- set.headers["location"] = `/show/${uuid}`
82- }
83- return `Created with id: ${uuid}`
84-
85- }, {
86- body: t.Object({
87- file: t.File(),
88- filename: t.Optional(t.String()),
89- //filetype: t.String()/*todo:verifiy via t.Enum*/,
90- filetype: t.Enum(stringArrayToEnum(filetypes)),
91- password: t.Optional(t.String()),
92- encrypted: t.Optional(t.String()),
93- delete_in_minutes: t.String({ format: "regex", pattern: "(^$|^[0-9]+$)" })
94- }),
95- query: t.Object({ withJs: t.Optional(t.Boolean()) })
96- })
97- .get("/show/:uuid", async ({ set, params, cookie }) => {
98-
99- const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
100- { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
101- if (!result) {
102- set.status = StatusMap["Not Found"]
103- return NotFound()
104- }
105- if (result.encrypted) {
106- const password = cookie.password.value
107- if (!password) {
108- return DecryptFile(result.filename, params.uuid)
109- } else {
110- try {
111- result.content = decrypt(result.content, password)
112- } catch (e) {
113- set.status = StatusMap["Forbidden"]
114- set.headers["set-cookie"] = [
115- `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
116- `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
117- ]
118- return WrongPassword()
119- }
120- }
121- }
122- return ShowFile(result.filename, params.uuid, result.content, result.filetype, result.delete_at)
123-
124- }, { params: t.Object({ uuid: t.String() }) })
125- .get("/show-js/:uuid", async ({ set, params, cookie }) => {
126-
127- const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
128- { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
129- if (!result) {
130- set.status = StatusMap["Not Found"]
131- return NotFound()
132- }
133- return ShowFile(result.filename, params.uuid, null, result.filetype, result.delete_at)
134-
135- }, { params: t.Object({ uuid: t.String() }) })
136- .post("/set-cookie/:uuid", ({ set, body, params }) => {
137-
138- set.headers["set-cookie"] = [
139- `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
140- `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`
141- ]
142- set.headers["location"] = `/show/${params.uuid}`
143- set.status = StatusMap["See Other"]
144- return SetCookie(params.uuid)
145-
146- }, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) })
147- .get("/raw/:uuid", ({ set, params, cookie, query }) => {
148- const result = db.prepare("SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?").get(params.uuid) as { content: Uint8Array, filename: string, encrypted: boolean, filetype: string } || null
149- if (!result) {
150- set.status = StatusMap["Not Found"]
151- return "File not found"
152- }
153- if (result.encrypted && query.ignore_password !== "true") {
154- if (!cookie.password.value) {
155- set.status = StatusMap["Unauthorized"]
156- return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it"
157- }
158- try {
159- result.content = decrypt(result.content, cookie.password.value)
160- } catch (e) {
161- set.status = StatusMap["Forbidden"]
162- return "Incorrect password"
163- }
164- }
165- set.headers["encrypted"] = result.encrypted ? "true" : "false"
166- set.headers["filetype"] = result.filetype
167- set.headers["filename"] = result.filename
168- set.headers["content-disposition"] = `inline; filename=${result.filename}`
169- return result.content
170-
171- }, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) })
172- .listen(3000)
59+ .use(staticPlugin({ assets: "./assets", prefix: "/" }))
60+ .use(html())
61+ .use(
62+ cron({
63+ name: "delete",
64+ pattern: "*/5 * * * * *",
65+ run() {
66+ db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')");
67+ },
68+ }),
69+ )
70+ .get("/", ({ server }) => Index(server?.url.toString() ?? "", false))
71+ .get("/js", ({ server }) => Index(server?.url.toString() ?? "", true))
72+ .post(
73+ "/upload",
74+ async ({ set, body, query }) => {
75+ const uuid = randomUUIDv7();
76+ let content = Buffer.from(await body.file.bytes());
77+ let encrypted = false;
78+ let delete_at: number | null = null;
79+ if (body.delete_in_minutes) {
80+ delete_at =
81+ Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60;
82+ }
83+ if (body.encrypted === "on") {
84+ encrypted = true;
85+ } else if (body.password) {
86+ content = encrypt(content, body.password);
87+ encrypted = true;
88+ }
89+ db.exec(
90+ "INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
91+ [
92+ uuid,
93+ body.filename || body.file.name,
94+ content,
95+ body.filetype,
96+ encrypted,
97+ delete_at,
98+ ],
99+ );
100+ set.status = StatusMap["See Other"];
101+ if (query.withJs) {
102+ set.headers.location = `/show-js/${uuid}`;
103+ } else {
104+ set.headers.location = `/show/${uuid}`;
105+ }
106+ return `Created with id: ${uuid}`;
107+ },
108+ {
109+ body: t.Object({
110+ file: t.File(),
111+ filename: t.Optional(t.String()),
112+ //filetype: t.String()/*todo:verifiy via t.Enum*/,
113+ filetype: t.Enum(stringArrayToEnum(filetypes)),
114+ password: t.Optional(t.String()),
115+ encrypted: t.Optional(t.String()),
116+ delete_in_minutes: t.String({
117+ format: "regex",
118+ pattern: "(^$|^[0-9]+$)",
119+ }),
120+ }),
121+ query: t.Object({ withJs: t.Optional(t.Boolean()) }),
122+ },
123+ )
124+ .get(
125+ "/show/:uuid",
126+ async ({ set, params, cookie }) => {
127+ const result =
128+ (db
129+ .prepare(
130+ "SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?",
131+ )
132+ .get(params.uuid) as {
133+ filename: string;
134+ content: Uint8Array;
135+ filetype: string;
136+ encrypted: boolean;
137+ delete_at: number | null;
138+ }) || null;
139+ if (!result) {
140+ set.status = StatusMap["Not Found"];
141+ return NotFound();
142+ }
143+ if (result.encrypted) {
144+ const password = cookie.password.value;
145+ if (!password) {
146+ return DecryptFile(result.filename, params.uuid);
147+ } else {
148+ try {
149+ result.content = decrypt(result.content, password);
150+ } catch (_e) {
151+ set.status = StatusMap.Forbidden;
152+ set.headers["set-cookie"] = [
153+ `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
154+ `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
155+ ];
156+ return WrongPassword();
157+ }
158+ }
159+ }
160+ return ShowFile(
161+ result.filename,
162+ params.uuid,
163+ result.content,
164+ result.filetype,
165+ result.delete_at,
166+ );
167+ },
168+ {
169+ params: t.Object({ uuid: t.String() }),
170+ cookie: t.Object({ password: t.Optional(t.String()) }),
171+ },
172+ )
173+ .get(
174+ "/show-js/:uuid",
175+ async ({ set, params }) => {
176+ const result =
177+ (db
178+ .prepare(
179+ "SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?",
180+ )
181+ .get(params.uuid) as {
182+ filename: string;
183+ content: Uint8Array;
184+ filetype: string;
185+ encrypted: boolean;
186+ delete_at: number | null;
187+ }) || null;
188+ if (!result) {
189+ set.status = StatusMap["Not Found"];
190+ return NotFound();
191+ }
192+ return ShowFile(
193+ result.filename,
194+ params.uuid,
195+ null,
196+ result.filetype,
197+ result.delete_at,
198+ );
199+ },
200+ { params: t.Object({ uuid: t.String() }) },
201+ )
202+ .post(
203+ "/set-cookie/:uuid",
204+ ({ set, body, params }) => {
205+ set.headers["set-cookie"] = [
206+ `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
207+ `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`,
208+ ];
209+ set.headers.location = `/show/${params.uuid}`;
210+ set.status = StatusMap["See Other"];
211+ return SetCookie(params.uuid);
212+ },
213+ {
214+ body: t.Object({ password: t.String() }),
215+ params: t.Object({ uuid: t.String() }),
216+ },
217+ )
218+ .get(
219+ "/raw/:uuid",
220+ ({ set, params, cookie, query }) => {
221+ const result =
222+ (db
223+ .prepare(
224+ "SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?",
225+ )
226+ .get(params.uuid) as {
227+ content: Uint8Array;
228+ filename: string;
229+ encrypted: boolean;
230+ filetype: string;
231+ }) || null;
232+ if (!result) {
233+ set.status = StatusMap["Not Found"];
234+ return "File not found";
235+ }
236+ if (result.encrypted && query.ignore_password !== "true") {
237+ if (!cookie.password.value) {
238+ set.status = StatusMap.Unauthorized;
239+ return 'This file is encrypted, set the cookie "password" with the correct password to allow the server to decrypt it';
240+ }
241+ try {
242+ result.content = decrypt(result.content, cookie.password.value);
243+ } catch (_e) {
244+ set.status = StatusMap.Forbidden;
245+ return "Incorrect password";
246+ }
247+ }
248+ set.headers.encrypted = result.encrypted ? "true" : "false";
249+ set.headers.filetype = result.filetype;
250+ set.headers.filename = result.filename;
251+ set.headers["content-disposition"] =
252+ `inline; filename=${result.filename}`;
253+ return result.content;
254+ },
255+ {
256+ params: t.Object({ uuid: t.String() }),
257+ cookie: t.Object({ password: t.Optional(t.String()) }),
258+ query: t.Object({ ignore_password: t.Optional(t.String()) }),
259+ },
260+ )
261+ .listen(3000);
173262
174-console.log(`⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`)
263+console.log(
264+ `⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`,
265+);
Msrc/shared.ts
@@ -1,5 +1,5 @@
11 export function humanFileSize(size: number) {
2- if (size < 1024) return `${size} B`
3- let i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)))
4- return `${(size / Math.pow(1024, i)).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`
2+ if (size < 1024) return `${size} B`;
3+ const i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)));
4+ return `${(size / 1024 ** i).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`;
55 }
Mtsconfig.json
@@ -1,18 +1,18 @@
11 {
2- "compilerOptions": {
3- /* Visit https://aka.ms/tsconfig to read more about this file */
4- "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5- "module": "ES2022", /* Specify what module code is generated. */
6- "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
7- "types": [
8- "bun-types"
9- ], /* Specify type package names to be included without being referenced in a source file. */
10- "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
11- "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
12- "strict": true, /* Enable all strict type-checking options. */
13- "skipLibCheck": true, /* Skip type checking all .d.ts files. */
14- "jsx": "react",
15- "jsxFactory": "Html.createElement",
16- "jsxFragmentFactory": "Html.Fragment"
17- }
18-}
2+ "compilerOptions": {
3+ /* Visit https://aka.ms/tsconfig to read more about this file */
4+ "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
5+ "module": "ES2022" /* Specify what module code is generated. */,
6+ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
7+ "types": [
8+ "bun-types"
9+ ] /* Specify type package names to be included without being referenced in a source file. */,
10+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
11+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
12+ "strict": true /* Enable all strict type-checking options. */,
13+ "skipLibCheck": true /* Skip type checking all .d.ts files. */,
14+ "jsx": "react",
15+ "jsxFactory": "Html.createElement",
16+ "jsxFragmentFactory": "Html.Fragment"
17+ }
18+}