components.tsx
Raw
1import { Html } from '@elysiajs/html'
2import { PropsWithChildren } from '@kitajs/html'
3import { escapeHTML } from 'bun'
4import { fileTypeFromBuffer } from 'file-type'
5import hljs from 'highlight.js'
6import isValidUTF8 from 'utf-8-validate'
7import { humanFileSize } 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 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 )
27}
28
29export function NotFound() {
30 return (
31 <Template css="/default.css">
32 <h1>404</h1>
33 <p>File not found</p>
34 </Template>
35 )
36}
37
38export 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 )
123}
124
125
126export 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 )
137}
138
139export 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 )
146}
147
148function 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;
155
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();
163}
164
165export 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 }
192
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 )
219}
220
221export 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 )
227}
228