components.tsx
Raw
1// biome-ignore lint/correctness/noUnusedImports: Html is the JSX factory (tsconfig jsxFactory: Html.createElement)
2import { Html } from "@elysiajs/html";
3import type { PropsWithChildren } from "@kitajs/html";
4import { escapeHTML } from "bun";
5import { fileTypeFromBuffer } from "file-type";
6import hljs from "highlight.js";
7import { humanFileSize, isValidUTF8 } from "./shared";
8
9export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort());
10
11function 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
22 rel="icon"
23 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>"
24 />
25 </head>
26 <body>{props.children}</body>
27 </html>
28 </>
29 );
30}
31
32export function NotFound() {
33 return (
34 <Template css="/default.css">
35 <h1>404</h1>
36 <p>File not found</p>
37 </Template>
38 );
39}
40
41export function Index(hostname: string) {
42 return (
43 <Template css="/default.css">
44 <h1>⚡ZBin⚡</h1>
45 <dialog id="upload-dialog">File is being uploaded, please wait</dialog>
46 <form
47 action="/upload"
48 id="uploadForm"
49 method="post"
50 enctype="multipart/form-data"
51 onsubmit="return onUploadSubmit()"
52 >
53 <input type="reset" value="Reset form" />
54 <input required={true} type="file" id="file" name="file" />
55 <label for="filename">File name override:</label>
56 <small>Optional. If empty, will use the uploaded file name.</small>
57 <input
58 type="text"
59 id="filename"
60 name="filename"
61 placeholder="File name"
62 />
63 <label for="filetype">File type:</label>
64 <small>
65 Used for syntax highlighting, if the file is only containing valid
66 UTF-8 characters. Otherwise it will always be stored as a binary blob.
67 Can select <code>blob</code> to force this detection.
68 </small>
69 <select size="10" required={true} id="filetype" name="filetype">
70 {filetypes.map((filetype, i) => (
71 <option selected={i === 0} value={filetype}>
72 {filetype}
73 </option>
74 ))}
75 </select>
76 <label for="delete-in">Delete in:</label>
77 <small>
78 Optional. If set, file will be deleted after the given number of
79 minutes.
80 </small>
81 <input
82 type="number"
83 id="delete-in"
84 name="delete_in_minutes"
85 min="0"
86 placeholder="0"
87 />
88 <label for="password">Password:</label>
89 <small>
90 Optional. If set, file will be encrypted and requires the given
91 password to open.
92 </small>
93 <input
94 type="password"
95 id="password"
96 name="password"
97 placeholder="Password"
98 />
99 <label for="encrypt-mode-server">Encryption mode:</label>
100 <small>
101 Only relevant when a password is set. Client-side encrypts in your
102 browser so the password never reaches the server (requires JavaScript).
103 Server-side encrypts on upload.
104 </small>
105 <div>
106 <label>
107 <input
108 type="radio"
109 id="encrypt-mode-server"
110 name="encrypt_mode"
111 value="server"
112 checked={true}
113 />{" "}
114 Server-side
115 </label>
116 <label>
117 <input
118 type="radio"
119 name="encrypt_mode"
120 value="client"
121 disabled={true}
122 />{" "}
123 Client-side (in your browser)
124 </label>
125 </div>
126 <div>
127 <label for="encrypted">Already encrypted:</label>
128 <input type="checkbox" id="encrypted" name="encrypted" />
129 </div>
130 <small>
131 If set, file is already encrypted with an algorithm like in the python
132 file available below. Password is ignored in this case.
133 </small>
134 <input type="submit" value="Upload File" />
135 <hr />
136 <h3>curl guide</h3>
137 You can use curl to upload and download files
138 <br />
139 Upload:
140 <pre>
141 curl {hostname}upload \{"\n"}
142 -F file=@/path/to/file \{"\n"}
143 -F filetype="plaintext" \{"\n"}# optional{"\n"}
144 -F filename="file name" \{"\n"}# optional{"\n"}
145 -F delete_in_minutes="60" \{"\n"}# optional{"\n"}
146 -F password="mypassword" \{"\n"}# optional{"\n"}
147 -F encrypted="on"
148 </pre>
149 <p>
150 If you upload an already encrypted file, you should set the{" "}
151 <code>encrypted="on"</code> parameter and encrypt it in a compatible
152 way, so the frontend/backend can also decrypt it. You can use this
153 python script to encrypt a file locally:{" "}
154 <a href="/encrypt.py">encrypt.py</a>
155 </p>
156 <br />
157 Download:
158 <pre>
159 curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}# optional,
160 if encrypted with password{"\n"}
161 --cookie "password=mypassword"
162 </pre>
163 <p>
164 If ignore_password is set to true, then encrypted files can be
165 downloaded directly (in encrypted form) without supplying the
166 password.
167 </p>
168 </form>
169 <script src="/dist/client-index.js" />
170 </Template>
171 );
172}
173
174export function WrongPassword() {
175 return (
176 <Template css="/default.css">
177 <h1>Incorrect password</h1>
178 <p>Reload page to try again</p>
179 </Template>
180 );
181}
182
183function humanReadableTime(minutes: number) {
184 const years = Math.floor(minutes / 525600);
185 minutes %= 525600;
186 const days = Math.floor(minutes / 1440);
187 minutes %= 1440;
188 const hours = Math.floor(minutes / 60);
189 minutes %= 60;
190
191 let result = "";
192 if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `;
193 if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `;
194 if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `;
195 if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `;
196 if (result === "") result = "0 minutes";
197 return result.trim();
198}
199
200export async function ShowFile(
201 filename: string,
202 uuid: string,
203 content: Uint8Array | null,
204 filetype: string,
205 delete_at: number | null,
206) {
207 //if content is null, this is for the js frontend
208 let preview: JSX.Element;
209 if (!content) {
210 preview = <>Please wait for the file to load</>;
211 } else {
212 preview = <>This file can't be previewed</>;
213 if (isValidUTF8(content) && filetype !== "blob") {
214 if (filetype === "none") {
215 preview = (
216 <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre>
217 );
218 } else {
219 preview = (
220 <pre>
221 {
222 hljs.highlight(new TextDecoder("utf-8").decode(content), {
223 language: filetype,
224 }).value
225 }
226 </pre>
227 );
228 }
229 } else {
230 const filetype = await fileTypeFromBuffer(content);
231 if (filetype) {
232 const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`;
233 if (filetype.mime.startsWith("audio/")) {
234 preview = <audio controls="" src={base64Content} />;
235 } else if (filetype.mime.startsWith("video/")) {
236 preview = <video controls src={base64Content} />;
237 } else if (filetype.mime.startsWith("image/")) {
238 preview = <img src={base64Content} alt={filename} />;
239 }
240 }
241 }
242 }
243
244 return (
245 <Template css="/show.css">
246 {content ? (
247 ""
248 ) : (
249 <div id="decrypt-overlay">
250 <h1>Encrypted file: {filename}</h1>
251 <form
252 id="decrypt-form"
253 action={`/set-cookie/${uuid}`}
254 method="post"
255 onsubmit="return onPasswordSubmit()"
256 >
257 <label id="password-label" for="password">
258 Enter a password to decrypt the file:
259 </label>
260 <input
261 required
262 type="password"
263 id="password"
264 name="password"
265 />
266 <div>
267 <label>
268 <input
269 type="radio"
270 name="decrypt_mode"
271 value="server"
272 checked={true}
273 />{" "}
274 Server-side
275 </label>
276 <label>
277 <input
278 type="radio"
279 name="decrypt_mode"
280 value="client"
281 disabled={true}
282 />{" "}
283 Client-side (in your browser)
284 </label>
285 </div>
286 <input type="submit" value="Submit" />
287 </form>
288 </div>
289 )}
290 <div id="content">
291 <div id="filename">{filename}</div>
292 <div id="mediabox">{preview}</div>
293 </div>
294 <div id="sidebar">
295 <h3>File info</h3>
296 <hr />
297 <p id="filesize">
298 size: {content ? humanFileSize(content.byteLength) : "unknown"}
299 </p>
300 <p>declared type: {filetype}</p>
301 {delete_at ? (
302 <p>
303 delete at: {new Date(delete_at * 1000).toISOString()} (in{" "}
304 {humanReadableTime(
305 Math.floor((delete_at - Date.now() / 1000) / 60),
306 )}
307 )
308 </p>
309 ) : (
310 ""
311 )}
312 <form id="download-form" action={`/raw/${uuid}`} method="get">
313 <button type="submit">Download</button>
314 </form>
315 </div>
316 {content ? "" : <script src="/dist/client-show.js" />}
317 </Template>
318 );
319}
320
321export function SetCookie(uuid: string) {
322 return (
323 <Template css="/default.css">
324 Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
325 </Template>
326 );
327}
328