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 isValidUTF8 from "utf-8-validate";
8import { humanFileSize } from "./shared";
9
10export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort());
11
12function Template(props: PropsWithChildren<{ css: string }>) {
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 );
31}
32
33export function NotFound() {
34 return (
35 <Template css="/default.css">
36 <h1>404</h1>
37 <p>File not found</p>
38 </Template>
39 );
40}
41
42export function Index(hostname: string, withJs: boolean) {
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 );
174}
175
176export function DecryptFile(filename: string, uuid: string) {
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 );
187}
188
189export function WrongPassword() {
190 return (
191 <Template css="/default.css">
192 <h1>Incorrect password</h1>
193 <p>Reload page to try again</p>
194 </Template>
195 );
196}
197
198function humanReadableTime(minutes: number) {
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;
205
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();
213}
214
215export 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 }
258
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 );
299}
300
301export function SetCookie(uuid: string) {
302 return (
303 <Template css="/default.css">
304 Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a>
305 </Template>
306 );
307}
308