biome integration & cleanup

AuthorKonata <konata@posteo.jp>
Date
Commit82a92c3cb14744ce391b1fa4dd72b788ee55c0d2
Parent0cd1857
12 files changed, 985 insertions(+), 707 deletions(-)
▾M.gitignore
@@ -1,5 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
/.idea
# dependencies
/node_modules
/.pnp
@@ -41,4 +41,4 @@ yarn-error.log*
package-lock.json
**/*.bun
db
assets/dist
assets/dist
▾Massets/default.css
@@ -2,87 +2,86 @@
@import "sakura-vader.css";
#jsguard {
display: none;
display: none;
}
body {
display: flex;
display: flex;
align-items: center;
flex-direction: column;
min-height: 100vh;
padding: 3rem;
padding: 3rem;
box-sizing: border-box;
max-width: 80rem;
max-width: 80rem;
}
a {
text-wrap: nowrap;
text-wrap: nowrap;
}
hr {
width: 100%;
width: 100%;
}
h1 {
margin-bottom: 4rem;
margin-bottom: 4rem;
}
form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
min-width: 35rem;
width: 100%;
input {
flex-grow: 1;
}
input[type=checkbox] {
flex-grow: 0;
margin-bottom: 0.5rem;
}
div {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
min-width: 35rem;
width: 100%;
input {
flex-grow: 1;
}
input[type="checkbox"] {
flex-grow: 0;
margin-bottom: 0.5rem;
}
div {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}
}
select {
overflow: auto;
flex-grow: 1;
overflow: auto;
flex-grow: 1;
}
img {
margin-bottom: 0;
margin-bottom: 0;
}
.help-icon {
width: 2rem;
height: 2rem;
filter: invert();
margin-left: 0.5rem;
width: 2rem;
height: 2rem;
filter: invert();
margin-left: 0.5rem;
}
input[type=reset] {
max-width: 25rem;
align-self: center;
input[type="reset"] {
max-width: 25rem;
align-self: center;
}
dialog {
background-color: #120c0e;
color: #d9d8dc;
border-color: #eb99a1;
form {
display: flex;
flex-direction: column;
align-items: center;
}
}
background-color: #120c0e;
color: #d9d8dc;
border-color: #eb99a1;
form {
display: flex;
flex-direction: column;
align-items: center;
}
}
▾Massets/show.css
@@ -3,92 +3,91 @@
@import "highlight.css";
hr {
width: 100%;
width: 100%;
}
body {
box-sizing: border-box;
width: 100%;
height: 100vh;
max-width: 100%;
max-height: 100vh;
box-sizing: border-box;
width: 100%;
height: 100vh;
max-width: 100%;
max-height: 100vh;
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
#content {
box-sizing: border-box;
flex-grow: 1;
height: 100%;
overflow: auto;
border: 1px solid #40363a;
border-right: unset;
display: flex;
flex-direction: column;
#content {
box-sizing: border-box;
flex-grow: 1;
height: 100%;
overflow: auto;
border: 1px solid #40363a;
border-right: unset;
display: flex;
flex-direction: column;
#filename {
padding-left: 1rem;
}
#filename {
padding-left: 1rem;
}
pre {
box-sizing: border-box;
margin: 0;
flex-grow: 1;
overflow: auto;
max-width: 100%;
max-height: 100%;
}
pre {
box-sizing: border-box;
margin: 0;
flex-grow: 1;
overflow: auto;
max-width: 100%;
max-height: 100%;
}
#mediabox {
flex-grow: 1;
display: flex;
justify-content: center;
}
}
#mediabox {
flex-grow: 1;
display: flex;
justify-content: center;
}
}
#sidebar {
box-sizing: border-box;
box-sizing: border-box;
flex-shrink: 0;
padding: 1rem;
width: 30rem;
height: 100%;
border: 1px solid #40363a;
display: flex;
flex-direction: column;
#sidebar {
box-sizing: border-box;
flex-shrink: 0;
padding: 1rem;
width: 30rem;
height: 100%;
border: 1px solid #40363a;
display: flex;
flex-direction: column;
h3 {
margin-top: 0;
text-align: center;
}
h3 {
margin-top: 0;
text-align: center;
}
form {
align-self: center;
}
}
form {
align-self: center;
}
}
}
audio {
max-width: 40rem;
min-width: 0;
flex-grow: 1;
align-self: center;
max-width: 40rem;
min-width: 0;
flex-grow: 1;
align-self: center;
}
img,
video {
object-fit: scale-down;
object-fit: scale-down;
}
dialog {
background-color: #120c0e;
color: #d9d8dc;
border-color: #eb99a1;
background-color: #120c0e;
color: #d9d8dc;
border-color: #eb99a1;
form {
display: flex;
flex-direction: column;
align-items: center;
}
}
/* biome-ignore lint/style/noDescendingSpecificity: targets dialog forms only, disjoint from the #sidebar form rule */
form {
display: flex;
flex-direction: column;
align-items: center;
}
}
▾Abiome.json
@@ -0,0 +1,44 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"src/**",
"*.json",
"!bun.lock",
"assets/show.css",
"assets/default.css"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useMediaCaption": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
▾Mbun.lock
@@ -16,6 +16,7 @@
"utf-8-validate": "^6.0.5",
},
"devDependencies": {
"@biomejs/biome": "2.4.16",
"@types/pkcs7-padding": "^0.1.3",
"@types/utf-8-validate": "^5.0.2",
"bun-types": "latest",
@@ -23,6 +24,24 @@
},
},
"packages": {
"@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=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
"@chneau/elysia-compression": ["@chneau/elysia-compression@1.0.11", "", { "dependencies": { "elysia": "^1.1.9" } }, "sha512-J4wfz5Qs68p/DNvkg5DXxo8sxJKSibzaUBP4W56uvqgs9NtHG0LC6tnyv7sAPEjNKzh21l6kinMoXbtVDp/KSQ=="],
▾Mpackage.json
@@ -1,26 +1,29 @@
{
"name": "zbin",
"version": "1.0.0",
"scripts": {
"build": "bun build src/client*.ts --outdir assets/dist",
"dev": "bun run build && bun run --watch src/index.ts",
"prod": "bun run build && bun run src/index.ts"
},
"dependencies": {
"@chneau/elysia-compression": "^1.0.11",
"@elysiajs/cron": "^1.2.0",
"@elysiajs/html": "^1.2.0",
"@elysiajs/static": "^1.2.0",
"elysia": "^1.2.0",
"file-type": "^20.0.1",
"highlight.js": "^11.11.1",
"pkcs7-padding": "^0.1.1",
"utf-8-validate": "^6.0.5"
},
"devDependencies": {
"@types/pkcs7-padding": "^0.1.3",
"@types/utf-8-validate": "^5.0.2",
"bun-types": "latest"
},
"module": "src/index.js"
"name": "zbin",
"version": "1.0.0",
"scripts": {
"build": "bun build src/client*.ts --outdir assets/dist",
"dev": "bun run build && bun run --watch src/index.ts",
"prod": "bun run build && bun run src/index.ts",
"format": "biome format --write",
"lint": "biome lint"
},
"dependencies": {
"@chneau/elysia-compression": "^1.0.11",
"@elysiajs/cron": "^1.2.0",
"@elysiajs/html": "^1.2.0",
"@elysiajs/static": "^1.2.0",
"elysia": "^1.2.0",
"file-type": "^20.0.1",
"highlight.js": "^11.11.1",
"pkcs7-padding": "^0.1.1",
"utf-8-validate": "^6.0.5"
},
"devDependencies": {
"@biomejs/biome": "2.4.16",
"@types/pkcs7-padding": "^0.1.3",
"@types/utf-8-validate": "^5.0.2",
"bun-types": "latest"
},
"module": "src/index.js"
}
▾Msrc/client-index.ts
@@ -1,78 +1,102 @@
async function encrypt(content: Uint8Array, password: string): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(16))
const salt = crypto.getRandomValues(new Uint8Array(16))
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-512"
},
keyMaterial,
{ name: "AES-CBC", length: 256 },
false,
["encrypt"]
)
const encryptedContent = await crypto.subtle.encrypt(
{ name: "AES-CBC", iv: iv },
key,
content
)
return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)]).buffer
async function encrypt(
content: Uint8Array<ArrayBuffer>,
password: string,
): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(16));
const salt = crypto.getRandomValues(new Uint8Array(16));
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-512",
},
keyMaterial,
{ name: "AES-CBC", length: 256 },
false,
["encrypt"],
);
const encryptedContent = await crypto.subtle.encrypt(
{ name: "AES-CBC", iv: iv },
key,
content,
);
return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)])
.buffer;
}
async function uploadFile() {
(document.getElementById("upload-dialog") as HTMLDialogElement | null)?.showModal()
const passwordInput = document.getElementById("password") as HTMLInputElement | null
const fileInput = document.getElementById("file") as HTMLInputElement | null
const filenameInput = document.getElementById("filename") as HTMLInputElement | null
const encryptedInput = document.getElementById("encrypted") as HTMLInputElement | null
const form = document.getElementById("uploadForm") as HTMLFormElement | null
(
document.getElementById("upload-dialog") as HTMLDialogElement | null
)?.showModal();
const passwordInput = document.getElementById(
"password",
) as HTMLInputElement | null;
const fileInput = document.getElementById("file") as HTMLInputElement | null;
const filenameInput = document.getElementById(
"filename",
) as HTMLInputElement | null;
const encryptedInput = document.getElementById(
"encrypted",
) as HTMLInputElement | null;
const form = document.getElementById("uploadForm") as HTMLFormElement | null;
if (!passwordInput || !fileInput || !filenameInput || !encryptedInput || !form) {
console.error("Missing required elements.")
console.error(passwordInput, fileInput, filenameInput, encryptedInput, form)
return
}
if (
!passwordInput ||
!fileInput ||
!filenameInput ||
!encryptedInput ||
!form
) {
console.error("Missing required elements.");
console.error(
passwordInput,
fileInput,
filenameInput,
encryptedInput,
form,
);
return;
}
const password = passwordInput.value;
if (!password) {
form.submit()
return
}
const password = passwordInput.value;
if (!password) {
form.submit();
return;
}
const file = fileInput.files?.[0];
if (!file) {
return
}
const file = fileInput.files?.[0];
if (!file) {
return;
}
const content = await file.arrayBuffer()
const encryptedContent = await encrypt(new Uint8Array(content), password)
const content = await file.arrayBuffer();
const encryptedContent = await encrypt(new Uint8Array(content), password);
const myFile = new File([encryptedContent], file.name)
const dataTransfer = new DataTransfer()
dataTransfer.items.add(myFile)
const myFile = new File([encryptedContent], file.name);
const dataTransfer = new DataTransfer();
dataTransfer.items.add(myFile);
fileInput.files = dataTransfer.files
encryptedInput.checked = true
console.log("encryption done")
form.submit()
fileInput.files = dataTransfer.files;
encryptedInput.checked = true;
console.log("encryption done");
form.submit();
}
const jsguards = document.getElementsByClassName("jsguard")
const jsguards = document.getElementsByClassName("jsguard");
for (let i = 0; i < jsguards.length; i++) {
jsguards[i].remove()
jsguards[i].remove();
}
(document.getElementById("uploadForm") as HTMLFormElement | null)?.reset();
window.uploadFile = uploadFile
window.uploadFile = uploadFile;
//disable bfcache, otherwise dialog will stay open when navigating back
window.addEventListener('unload', function () { });
window.addEventListener('beforeunload', function () { });
window.addEventListener("unload", () => {});
window.addEventListener("beforeunload", () => {});
▾Msrc/client-show.ts
@@ -1,139 +1,158 @@
import { fileTypeFromBuffer } from 'file-type'
import hljs from 'highlight.js'
import { humanFileSize } from './shared'
import { fileTypeFromBuffer } from "file-type";
import hljs from "highlight.js";
import { humanFileSize } from "./shared";
async function decrypt(encryptedContent: ArrayBuffer, password: string): Promise<ArrayBuffer> {
const encryptedArray = new Uint8Array(encryptedContent)
const salt = encryptedArray.slice(0, 16)
const iv = encryptedArray.slice(16, 32)
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-512"
},
keyMaterial,
{ name: "AES-CBC", length: 256 },
false,
["decrypt"]
)
const decryptedContent = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv: iv },
key,
encryptedArray.slice(32)
)
return decryptedContent
async function decrypt(
encryptedContent: ArrayBuffer,
password: string,
): Promise<ArrayBuffer> {
const encryptedArray = new Uint8Array(encryptedContent);
const salt = encryptedArray.slice(0, 16);
const iv = encryptedArray.slice(16, 32);
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-512",
},
keyMaterial,
{ name: "AES-CBC", length: 256 },
false,
["decrypt"],
);
const decryptedContent = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv: iv },
key,
encryptedArray.slice(32),
);
return decryptedContent;
}
function isValidUTF8(buf: ArrayBuffer) {
try {
new TextDecoder("utf-8", { fatal: true }).decode(buf)
return true
} catch (e) {
return false
}
try {
new TextDecoder("utf-8", { fatal: true }).decode(buf);
return true;
} catch (_e) {
return false;
}
}
async function showContent(content: ArrayBuffer, filetype: string) {
const filesize = document.getElementById("filesize") as HTMLDivElement | null
if (filesize) {
filesize.textContent = `size: ${humanFileSize(content.byteLength)}`
}
const filesize = document.getElementById("filesize") as HTMLDivElement | null;
if (filesize) {
filesize.textContent = `size: ${humanFileSize(content.byteLength)}`;
}
const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null
if (!mediaBox) {
console.error("Missing mediaBox element.")
return
}
let preview: HTMLElement = document.createElement("div")
preview.textContent = "This file can't be previewed"
if (isValidUTF8(content) && filetype !== "blob") {
preview = document.createElement("pre")
if (filetype === "none") {
preview.textContent = new TextDecoder().decode(content)
} else {
preview.innerHTML = hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value
}
} else {
const filetype = await fileTypeFromBuffer(content)
if (filetype) {
const blob = new Blob([content], { type: filetype.mime })
const url = URL.createObjectURL(blob)
if (filetype.mime.startsWith("audio/")) {
preview = document.createElement("audio")
preview.setAttribute("controls", "")
preview.setAttribute("src", url)
} else if (filetype.mime.startsWith("video/")) {
preview = document.createElement("video")
preview.setAttribute("controls", "")
preview.setAttribute("src", url)
} else if (filetype.mime.startsWith("image/")) {
preview = document.createElement("img")
preview.setAttribute("src", url)
}
}
}
mediaBox.innerHTML = ""
mediaBox.appendChild(preview)
const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null;
if (!mediaBox) {
console.error("Missing mediaBox element.");
return;
}
let preview: HTMLElement = document.createElement("div");
preview.textContent = "This file can't be previewed";
if (isValidUTF8(content) && filetype !== "blob") {
preview = document.createElement("pre");
if (filetype === "none") {
preview.textContent = new TextDecoder().decode(content);
} else {
preview.innerHTML = hljs.highlight(
new TextDecoder("utf-8").decode(content),
{ language: filetype },
).value;
}
} else {
const filetype = await fileTypeFromBuffer(content);
if (filetype) {
const blob = new Blob([content], { type: filetype.mime });
const url = URL.createObjectURL(blob);
if (filetype.mime.startsWith("audio/")) {
preview = document.createElement("audio");
preview.setAttribute("controls", "");
preview.setAttribute("src", url);
} else if (filetype.mime.startsWith("video/")) {
preview = document.createElement("video");
preview.setAttribute("controls", "");
preview.setAttribute("src", url);
} else if (filetype.mime.startsWith("image/")) {
preview = document.createElement("img");
preview.setAttribute("src", url);
}
}
}
mediaBox.innerHTML = "";
mediaBox.appendChild(preview);
}
let content: ArrayBuffer
let filetype: string
let filename: string
let content: ArrayBuffer;
let filetype: string;
let filename: string;
async function onPasswordSubmit() {
const passwordInput = document.getElementById("password") as HTMLInputElement | null
const passwordLabel = document.getElementById("password-label") as HTMLLabelElement | null
if (!passwordInput || !passwordLabel) {
console.error("Missing passwordInput or passwordLabel element.")
return
}
try {
content = await decrypt(content, passwordInput.value)
} catch (e) {
passwordLabel.textContent = "Incorrect password"
return
}
const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null
dialog?.close()
const downloadForm = document.getElementById("download-form") as HTMLFormElement | null
downloadForm?.setAttribute("action", "")
downloadForm?.addEventListener("submit", (e) => {
e.preventDefault()
const blob = new Blob([content])
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.download = filename
link.href = url
link.click();
})
showContent(content, filetype)
const passwordInput = document.getElementById(
"password",
) as HTMLInputElement | null;
const passwordLabel = document.getElementById(
"password-label",
) as HTMLLabelElement | null;
if (!passwordInput || !passwordLabel) {
console.error("Missing passwordInput or passwordLabel element.");
return;
}
try {
content = await decrypt(content, passwordInput.value);
} catch (_e) {
passwordLabel.textContent = "Incorrect password";
return;
}
const dialog = document.getElementById(
"password-dialog",
) as HTMLDialogElement | null;
dialog?.close();
const downloadForm = document.getElementById(
"download-form",
) as HTMLFormElement | null;
downloadForm?.setAttribute("action", "");
downloadForm?.addEventListener("submit", (e) => {
e.preventDefault();
const blob = new Blob([content]);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = filename;
link.href = url;
link.click();
});
showContent(content, filetype);
}
const uuid = window.location.pathname.split("/").filter((x) => x !== "").reverse()[0]
const uuid = window.location.pathname
.split("/")
.filter((x) => x !== "")
.reverse()[0];
fetch("/raw/" + uuid + "?ignore_password=true").then(async (response) => {
content = await response.arrayBuffer()
const mediaBox = document.getElementById("mediabox")
if(mediaBox) {
mediaBox.innerHTML = ""
}
filetype = response.headers.get("filetype") || "none"
filename = response.headers.get("filename") || ""
if (response.headers.get("encrypted") === "true") {
const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null
dialog?.showModal()
} else {
showContent(content, filetype)
}
fetch(`/raw/${uuid}?ignore_password=true`).then(async (response) => {
content = await response.arrayBuffer();
const mediaBox = document.getElementById("mediabox");
if (mediaBox) {
mediaBox.innerHTML = "";
}
filetype = response.headers.get("filetype") || "none";
filename = response.headers.get("filename") || "";
if (response.headers.get("encrypted") === "true") {
const dialog = document.getElementById(
"password-dialog",
) as HTMLDialogElement | null;
dialog?.showModal();
} else {
showContent(content, filetype);
}
});
(window as any).onPasswordSubmit = onPasswordSubmit
Object.assign(window, { onPasswordSubmit });
▾Msrc/components.tsx
@@ -1,227 +1,307 @@
import { Html } from '@elysiajs/html'
import { PropsWithChildren } from '@kitajs/html'
import { escapeHTML } from 'bun'
import { fileTypeFromBuffer } from 'file-type'
import hljs from 'highlight.js'
import isValidUTF8 from 'utf-8-validate'
import { humanFileSize } from './shared'
// biome-ignore lint/correctness/noUnusedImports: Html is the JSX factory (tsconfig jsxFactory: Html.createElement)
import { Html } from "@elysiajs/html";
import type { PropsWithChildren } from "@kitajs/html";
import { escapeHTML } from "bun";
import { fileTypeFromBuffer } from "file-type";
import hljs from "highlight.js";
import isValidUTF8 from "utf-8-validate";
import { humanFileSize } from "./shared";
export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort())
export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort());
function Template(props: PropsWithChildren<{ css: string }>) {
return (
<>
{'<!doctype html>'}
<html lang="en">
<head>
<title>⚡ZBin⚡</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={props.css} />
<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>" />
</head>
<body>{props.children}</body>
</html>
</>
)
return (
<>
{"<!doctype html>"}
<html lang="en">
<head>
<title>⚡ZBin⚡</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={props.css} />
<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>"
/>
</head>
<body>{props.children}</body>
</html>
</>
);
}
export function NotFound() {
return (
<Template css="/default.css">
<h1>404</h1>
<p>File not found</p>
</Template>
)
return (
<Template css="/default.css">
<h1>404</h1>
<p>File not found</p>
</Template>
);
}
export function Index(hostname: string, withJs: boolean) {
let body = (
<>
<dialog id="upload-dialog">
File is being uploaded, please wait
</dialog>
{withJs ?
<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>
: <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>
}
<hr />
<form action={withJs ? "/upload?withJs=true" : "/upload"} id="uploadForm" method="post" enctype="multipart/form-data" onsubmit={withJs ? "uploadFile(); return false" : undefined}>
<input type="reset" value="Reset form" />
<input required={true} type="file" id="file" name="file" />
<label for="filename">File name override:</label>
<small>Optional. If empty, will use the uploaded file name.</small>
<input type="text" id="filename" name="filename" placeholder="File name" />
<label for="filetype">File type:</label>
<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>
<select size="10" required={true} id="filetype" name="filetype">
{
filetypes.map((filetype, i) => (
<option selected={i === 0} value={filetype}>{filetype}</option>
))
}
</select>
<label for="delete-in">Delete in:</label>
<small>Optional. If set, file will be deleted after the given number of minutes.</small>
<input type="number" id="delete-in" name="delete_in_minutes" min="0" placeholder="0" />
<label for="password">Password:</label>
<small>Optional. If set, file will be encrypted and requires the given password to open.</small>
<input type="password" id="password" name="password" placeholder="Password" />
<div>
<label for="encrypted">Already encrypted:</label>
<input type="checkbox" id="encrypted" name="encrypted" />
</div>
<small>If set, file is already encrypted with an algorithm like in the python file available below. Password is ignored in this case.</small>
<input type="submit" value="Upload File" />
<hr />
<h3>curl guide</h3>
You can use curl to upload and download files<br />
Upload:
<pre>curl {hostname}upload \{"\n"}
-F file=@/path/to/file \{"\n"}
-F filetype="plaintext" \{"\n"}
# optional{"\n"}
-F filename="file name" \{"\n"}
# optional{"\n"}
-F delete_in_minutes="60" \{"\n"}
# optional{"\n"}
-F password="mypassword" \{"\n"}
# optional{"\n"}
-F encrypted="on"</pre>
<p>
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.
You can use this python script to encrypt a file locally: <a href="/encrypt.py">encrypt.py</a>
</p>
<br />
Download:
<pre>curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}
# optional, if encrypted with password{"\n"}
--cookie "password=mypassword"</pre>
<p>
If ignore_password is set to true, then encrypted files can be downloaded directly (in encrypted form) without supplying the password.
</p>
</form>
</>
)
if (withJs) {
body = (
<>
<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>
<div class="jsguard">
{body}
</div>
<script src="/dist/client-index.js" />
</>
)
}
return (
<Template css="/default.css">
<h1>⚡ZBin⚡</h1>
{body}
</Template>
)
let body = (
<>
<dialog id="upload-dialog">File is being uploaded, please wait</dialog>
{withJs ? (
<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>
) : (
<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>
)}
<hr />
<form
action={withJs ? "/upload?withJs=true" : "/upload"}
id="uploadForm"
method="post"
enctype="multipart/form-data"
onsubmit={withJs ? "uploadFile(); return false" : undefined}
>
<input type="reset" value="Reset form" />
<input required={true} type="file" id="file" name="file" />
<label for="filename">File name override:</label>
<small>Optional. If empty, will use the uploaded file name.</small>
<input
type="text"
id="filename"
name="filename"
placeholder="File name"
/>
<label for="filetype">File type:</label>
<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>
<select size="10" required={true} id="filetype" name="filetype">
{filetypes.map((filetype, i) => (
<option selected={i === 0} value={filetype}>
{filetype}
</option>
))}
</select>
<label for="delete-in">Delete in:</label>
<small>
Optional. If set, file will be deleted after the given number of
minutes.
</small>
<input
type="number"
id="delete-in"
name="delete_in_minutes"
min="0"
placeholder="0"
/>
<label for="password">Password:</label>
<small>
Optional. If set, file will be encrypted and requires the given
password to open.
</small>
<input
type="password"
id="password"
name="password"
placeholder="Password"
/>
<div>
<label for="encrypted">Already encrypted:</label>
<input type="checkbox" id="encrypted" name="encrypted" />
</div>
<small>
If set, file is already encrypted with an algorithm like in the python
file available below. Password is ignored in this case.
</small>
<input type="submit" value="Upload File" />
<hr />
<h3>curl guide</h3>
You can use curl to upload and download files
<br />
Upload:
<pre>
curl {hostname}upload \{"\n"}
-F file=@/path/to/file \{"\n"}
-F filetype="plaintext" \{"\n"}# optional{"\n"}
-F filename="file name" \{"\n"}# optional{"\n"}
-F delete_in_minutes="60" \{"\n"}# optional{"\n"}
-F password="mypassword" \{"\n"}# optional{"\n"}
-F encrypted="on"
</pre>
<p>
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. You can use this
python script to encrypt a file locally:{" "}
<a href="/encrypt.py">encrypt.py</a>
</p>
<br />
Download:
<pre>
curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}# optional,
if encrypted with password{"\n"}
--cookie "password=mypassword"
</pre>
<p>
If ignore_password is set to true, then encrypted files can be
downloaded directly (in encrypted form) without supplying the
password.
</p>
</form>
</>
);
if (withJs) {
body = (
<>
<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>
<div class="jsguard">{body}</div>
<script src="/dist/client-index.js" />
</>
);
}
return (
<Template css="/default.css">
<h1>⚡ZBin⚡</h1>
{body}
</Template>
);
}
export function DecryptFile(filename: string, uuid: string) {
return (
<Template css="/default.css">
<h1>Encrypted file: {filename}</h1>
<form action={`/set-cookie/${uuid}`} method="post">
<label for="password">Enter a Password to decrypt the file</label>
<input type="password" name="password" />
<input type="submit" value="Submit" />
</form>
</Template>
)
return (
<Template css="/default.css">
<h1>Encrypted file: {filename}</h1>
<form action={`/set-cookie/${uuid}`} method="post">
<label for="password">Enter a Password to decrypt the file</label>
<input type="password" name="password" />
<input type="submit" value="Submit" />
</form>
</Template>
);
}
export function WrongPassword() {
return (
<Template css="/default.css">
<h1>Incorrect password</h1>
<p>Reload page to try again</p>
</Template>
)
return (
<Template css="/default.css">
<h1>Incorrect password</h1>
<p>Reload page to try again</p>
</Template>
);
}
function humanReadableTime(minutes: number) {
const years = Math.floor(minutes / 525600);
minutes %= 525600;
const days = Math.floor(minutes / 1440);
minutes %= 1440;
const hours = Math.floor(minutes / 60);
minutes %= 60;
const years = Math.floor(minutes / 525600);
minutes %= 525600;
const days = Math.floor(minutes / 1440);
minutes %= 1440;
const hours = Math.floor(minutes / 60);
minutes %= 60;
let result = "";
if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `
if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `
if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `
if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `
if (result === "") result = "0 minutes"
return result.trim();
let result = "";
if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `;
if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `;
if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `;
if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `;
if (result === "") result = "0 minutes";
return result.trim();
}
export async function ShowFile(filename: string, uuid: string, content: Uint8Array | null, filetype: string, delete_at: number | null) {
//if content is null, this is for the js frontend
let preview;
if (!content) {
preview = <>Please wait for the file to load</>
} else {
preview = <>This file can't be previewed</>
if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") {
if (filetype === "none") {
preview = <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre>
} else {
preview = <pre>{hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value}</pre>
}
} else {
const filetype = await fileTypeFromBuffer(content)
if (filetype) {
const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`;
if (filetype.mime.startsWith("audio/")) {
preview = <audio controls="" src={base64Content} />
} else if (filetype.mime.startsWith("video/")) {
preview = <video controls src={base64Content} />
} else if (filetype.mime.startsWith("image/")) {
preview = <img src={base64Content} />
}
}
}
}
export async function ShowFile(
filename: string,
uuid: string,
content: Uint8Array | null,
filetype: string,
delete_at: number | null,
) {
//if content is null, this is for the js frontend
let preview: JSX.Element;
if (!content) {
preview = <>Please wait for the file to load</>;
} else {
preview = <>This file can't be previewed</>;
if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") {
if (filetype === "none") {
preview = (
<pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre>
);
} else {
preview = (
<pre>
{
hljs.highlight(new TextDecoder("utf-8").decode(content), {
language: filetype,
}).value
}
</pre>
);
}
} else {
const filetype = await fileTypeFromBuffer(content);
if (filetype) {
const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`;
if (filetype.mime.startsWith("audio/")) {
preview = <audio controls="" src={base64Content} />;
} else if (filetype.mime.startsWith("video/")) {
preview = <video controls src={base64Content} />;
} else if (filetype.mime.startsWith("image/")) {
preview = <img src={base64Content} alt={filename} />;
}
}
}
}
return (
<Template css="/show.css">
<dialog id="password-dialog">
<form onsubmit="onPasswordSubmit(); return false">
<label id="password-label" for="password">File is encrypted, enter password to decrypt:</label>
<input required type="password" id="password" />
<input type="submit" value="Submit" />
</form>
</dialog>
<div id="content">
<div id="filename">{filename}</div>
<div id="mediabox">{preview}</div>
</div>
<div id="sidebar">
<h3>File info</h3>
<hr />
<p id="filesize">size: {content ? humanFileSize(content.byteLength) : "unknown"}</p>
<p>declared type: {filetype}</p>
{delete_at ? <p>delete at: {new Date(delete_at * 1000).toISOString()} (in {humanReadableTime(Math.floor((delete_at - (Date.now() / 1000)) / 60))})</p> : ""}
<form id="download-form" action={`/raw/${uuid}`} method="get">
<button>Download</button>
</form>
</div>
{content ? "" : <script src="/dist/client-show.js" />}
</Template>
)
return (
<Template css="/show.css">
<dialog id="password-dialog">
<form onsubmit="onPasswordSubmit(); return false">
<label id="password-label" for="password">
File is encrypted, enter password to decrypt:
</label>
<input required type="password" id="password" />
<input type="submit" value="Submit" />
</form>
</dialog>
<div id="content">
<div id="filename">{filename}</div>
<div id="mediabox">{preview}</div>
</div>
<div id="sidebar">
<h3>File info</h3>
<hr />
<p id="filesize">
size: {content ? humanFileSize(content.byteLength) : "unknown"}
</p>
<p>declared type: {filetype}</p>
{delete_at ? (
<p>
delete at: {new Date(delete_at * 1000).toISOString()} (in{" "}
{humanReadableTime(
Math.floor((delete_at - Date.now() / 1000) / 60),
)}
)
</p>
) : (
""
)}
<form id="download-form" action={`/raw/${uuid}`} method="get">
<button type="submit">Download</button>
</form>
</div>
{content ? "" : <script src="/dist/client-show.js" />}
</Template>
);
}
export function SetCookie(uuid: string) {
return (
<Template css="/default.css">
Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
</Template>
)
return (
<Template css="/default.css">
Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
</Template>
);
}
▾Msrc/index.ts
@@ -1,174 +1,265 @@
import { Elysia, StatusMap, t } from "elysia"
import { Database } from "bun:sqlite"
import { randomUUIDv7 } from "bun"
import staticPlugin from "@elysiajs/static"
import { html } from '@elysiajs/html'
import crypto from 'crypto'
import { DecryptFile, filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components"
import cron from "@elysiajs/cron"
const db = new Database("./db/db.sqlite")
db.exec("PRAGMA foreign_keys = ON")
db.exec("PRAGMA journal_mode = WAL2")
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")
db.exec("PRAGMA optimize")
import { Database } from "bun:sqlite";
import crypto from "node:crypto";
import cron from "@elysiajs/cron";
import { html } from "@elysiajs/html";
import staticPlugin from "@elysiajs/static";
import { randomUUIDv7 } from "bun";
import { Elysia, StatusMap, t } from "elysia";
import {
DecryptFile,
filetypes,
Index,
NotFound,
SetCookie,
ShowFile,
WrongPassword,
} from "./components";
const db = new Database("./db/db.sqlite");
db.run("PRAGMA foreign_keys = ON");
db.run("PRAGMA journal_mode = WAL2");
db.run(
"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",
);
db.run("PRAGMA optimize");
function encrypt(content: Uint8Array, password: string): Buffer<ArrayBuffer> {
let iv = crypto.randomBytes(16)
const salt = crypto.randomBytes(16)
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
let cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
let encrypted_content = Buffer.concat([salt, iv, cipher.update(content), cipher.final()])
return encrypted_content
const iv = crypto.randomBytes(16);
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512");
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
return Buffer.concat([salt, iv, cipher.update(content), cipher.final()]);
}
function decrypt(encrypted_content: Uint8Array, password: string): Buffer {
const salt = encrypted_content.slice(0, 16)
const iv = encrypted_content.slice(16, 32)
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let content = Buffer.concat([decipher.update(encrypted_content.slice(32)), decipher.final()])
return content
const salt = encrypted_content.slice(0, 16);
const iv = encrypted_content.slice(16, 32);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512");
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
return Buffer.concat([
decipher.update(encrypted_content.slice(32)),
decipher.final(),
]);
}
function stringArrayToEnum<T extends string>(
arr: readonly T[]
arr: readonly T[],
): { [K in T]: K } {
return arr.reduce((acc, key) => {
acc[key] = key;
return acc;
}, Object.create(null));
return arr.reduce((acc, key) => {
acc[key] = key;
return acc;
}, Object.create(null));
}
const app = new Elysia({
serve: {
maxRequestBodySize: 1024 * 1024 * 1024 // 1GB
}
serve: {
maxRequestBodySize: 1024 * 1024 * 1024, // 1GB
},
})
.use(staticPlugin({ assets: "./assets", prefix: "/" }))
.use(html())
.use(cron({
name: "delete",
pattern: "*/5 * * * * *",
run() {
db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')")
}
}))
.get("/", ({ server }) => Index(server?.url.toString()!, false))
.get("/js", ({ server }) => Index(server?.url.toString()!, true))
.post("/upload", async ({ set, body, query }) => {
const uuid = randomUUIDv7()
let content = Buffer.from(await body.file.bytes())
let encrypted = false;
let delete_at: number | null = null
if (body.delete_in_minutes) {
delete_at = Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60
}
if (body.encrypted === "on") {
encrypted = true
} else if (body.password) {
content = encrypt(content, body.password)
encrypted = true
}
db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
[uuid, body.filename || body.file.name, content, body.filetype, encrypted, delete_at])
set.status = StatusMap["See Other"]
if (query.withJs) {
set.headers["location"] = `/show-js/${uuid}`
} else {
set.headers["location"] = `/show/${uuid}`
}
return `Created with id: ${uuid}`
}, {
body: t.Object({
file: t.File(),
filename: t.Optional(t.String()),
//filetype: t.String()/*todo:verifiy via t.Enum*/,
filetype: t.Enum(stringArrayToEnum(filetypes)),
password: t.Optional(t.String()),
encrypted: t.Optional(t.String()),
delete_in_minutes: t.String({ format: "regex", pattern: "(^$|^[0-9]+$)" })
}),
query: t.Object({ withJs: t.Optional(t.Boolean()) })
})
.get("/show/:uuid", async ({ set, params, cookie }) => {
const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
{ filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
if (!result) {
set.status = StatusMap["Not Found"]
return NotFound()
}
if (result.encrypted) {
const password = cookie.password.value
if (!password) {
return DecryptFile(result.filename, params.uuid)
} else {
try {
result.content = decrypt(result.content, password)
} catch (e) {
set.status = StatusMap["Forbidden"]
set.headers["set-cookie"] = [
`password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
`password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
]
return WrongPassword()
}
}
}
return ShowFile(result.filename, params.uuid, result.content, result.filetype, result.delete_at)
}, { params: t.Object({ uuid: t.String() }) })
.get("/show-js/:uuid", async ({ set, params, cookie }) => {
const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
{ filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
if (!result) {
set.status = StatusMap["Not Found"]
return NotFound()
}
return ShowFile(result.filename, params.uuid, null, result.filetype, result.delete_at)
}, { params: t.Object({ uuid: t.String() }) })
.post("/set-cookie/:uuid", ({ set, body, params }) => {
set.headers["set-cookie"] = [
`password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
`password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`
]
set.headers["location"] = `/show/${params.uuid}`
set.status = StatusMap["See Other"]
return SetCookie(params.uuid)
}, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) })
.get("/raw/:uuid", ({ set, params, cookie, query }) => {
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
if (!result) {
set.status = StatusMap["Not Found"]
return "File not found"
}
if (result.encrypted && query.ignore_password !== "true") {
if (!cookie.password.value) {
set.status = StatusMap["Unauthorized"]
return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it"
}
try {
result.content = decrypt(result.content, cookie.password.value)
} catch (e) {
set.status = StatusMap["Forbidden"]
return "Incorrect password"
}
}
set.headers["encrypted"] = result.encrypted ? "true" : "false"
set.headers["filetype"] = result.filetype
set.headers["filename"] = result.filename
set.headers["content-disposition"] = `inline; filename=${result.filename}`
return result.content
}, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) })
.listen(3000)
.use(staticPlugin({ assets: "./assets", prefix: "/" }))
.use(html())
.use(
cron({
name: "delete",
pattern: "*/5 * * * * *",
run() {
db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')");
},
}),
)
.get("/", ({ server }) => Index(server?.url.toString() ?? "", false))
.get("/js", ({ server }) => Index(server?.url.toString() ?? "", true))
.post(
"/upload",
async ({ set, body, query }) => {
const uuid = randomUUIDv7();
let content = Buffer.from(await body.file.bytes());
let encrypted = false;
let delete_at: number | null = null;
if (body.delete_in_minutes) {
delete_at =
Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60;
}
if (body.encrypted === "on") {
encrypted = true;
} else if (body.password) {
content = encrypt(content, body.password);
encrypted = true;
}
db.exec(
"INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
[
uuid,
body.filename || body.file.name,
content,
body.filetype,
encrypted,
delete_at,
],
);
set.status = StatusMap["See Other"];
if (query.withJs) {
set.headers.location = `/show-js/${uuid}`;
} else {
set.headers.location = `/show/${uuid}`;
}
return `Created with id: ${uuid}`;
},
{
body: t.Object({
file: t.File(),
filename: t.Optional(t.String()),
//filetype: t.String()/*todo:verifiy via t.Enum*/,
filetype: t.Enum(stringArrayToEnum(filetypes)),
password: t.Optional(t.String()),
encrypted: t.Optional(t.String()),
delete_in_minutes: t.String({
format: "regex",
pattern: "(^$|^[0-9]+$)",
}),
}),
query: t.Object({ withJs: t.Optional(t.Boolean()) }),
},
)
.get(
"/show/:uuid",
async ({ set, params, cookie }) => {
const result =
(db
.prepare(
"SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?",
)
.get(params.uuid) as {
filename: string;
content: Uint8Array;
filetype: string;
encrypted: boolean;
delete_at: number | null;
}) || null;
if (!result) {
set.status = StatusMap["Not Found"];
return NotFound();
}
if (result.encrypted) {
const password = cookie.password.value;
if (!password) {
return DecryptFile(result.filename, params.uuid);
} else {
try {
result.content = decrypt(result.content, password);
} catch (_e) {
set.status = StatusMap.Forbidden;
set.headers["set-cookie"] = [
`password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
`password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
];
return WrongPassword();
}
}
}
return ShowFile(
result.filename,
params.uuid,
result.content,
result.filetype,
result.delete_at,
);
},
{
params: t.Object({ uuid: t.String() }),
cookie: t.Object({ password: t.Optional(t.String()) }),
},
)
.get(
"/show-js/:uuid",
async ({ set, params }) => {
const result =
(db
.prepare(
"SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?",
)
.get(params.uuid) as {
filename: string;
content: Uint8Array;
filetype: string;
encrypted: boolean;
delete_at: number | null;
}) || null;
if (!result) {
set.status = StatusMap["Not Found"];
return NotFound();
}
return ShowFile(
result.filename,
params.uuid,
null,
result.filetype,
result.delete_at,
);
},
{ params: t.Object({ uuid: t.String() }) },
)
.post(
"/set-cookie/:uuid",
({ set, body, params }) => {
set.headers["set-cookie"] = [
`password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
`password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`,
];
set.headers.location = `/show/${params.uuid}`;
set.status = StatusMap["See Other"];
return SetCookie(params.uuid);
},
{
body: t.Object({ password: t.String() }),
params: t.Object({ uuid: t.String() }),
},
)
.get(
"/raw/:uuid",
({ set, params, cookie, query }) => {
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;
if (!result) {
set.status = StatusMap["Not Found"];
return "File not found";
}
if (result.encrypted && query.ignore_password !== "true") {
if (!cookie.password.value) {
set.status = StatusMap.Unauthorized;
return 'This file is encrypted, set the cookie "password" with the correct password to allow the server to decrypt it';
}
try {
result.content = decrypt(result.content, cookie.password.value);
} catch (_e) {
set.status = StatusMap.Forbidden;
return "Incorrect password";
}
}
set.headers.encrypted = result.encrypted ? "true" : "false";
set.headers.filetype = result.filetype;
set.headers.filename = result.filename;
set.headers["content-disposition"] =
`inline; filename=${result.filename}`;
return result.content;
},
{
params: t.Object({ uuid: t.String() }),
cookie: t.Object({ password: t.Optional(t.String()) }),
query: t.Object({ ignore_password: t.Optional(t.String()) }),
},
)
.listen(3000);
console.log(`⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`)
console.log(
`⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`,
);
▾Msrc/shared.ts
@@ -1,5 +1,5 @@
export function humanFileSize(size: number) {
if (size < 1024) return `${size} B`
let i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)))
return `${(size / Math.pow(1024, i)).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`
if (size < 1024) return `${size} B`;
const i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)));
return `${(size / 1024 ** i).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`;
}
▾Mtsconfig.json
@@ -1,18 +1,18 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "ES2022", /* Specify what module code is generated. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"types": [
"bun-types"
], /* Specify type package names to be included without being referenced in a source file. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment"
}
}
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "ES2022" /* Specify what module code is generated. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"types": [
"bun-types"
] /* Specify type package names to be included without being referenced in a source file. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment"
}
}