components.tsx
| 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' |
| 8 | |
| 9 | export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort()) |
| 10 | |
| 11 | 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 | ) |
| 27 | } |
| 28 | |
| 29 | export function NotFound() { |
| 30 | return ( |
| 31 | <Template css="/default.css"> |
| 32 | <h1>404</h1> |
| 33 | <p>File not found</p> |
| 34 | </Template> |
| 35 | ) |
| 36 | } |
| 37 | |
| 38 | 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 | ) |
| 123 | } |
| 124 | |
| 125 | |
| 126 | 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 | ) |
| 137 | } |
| 138 | |
| 139 | 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 | ) |
| 146 | } |
| 147 | |
| 148 | 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; |
| 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 | |
| 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 | } |
| 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 | |
| 221 | 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 | ) |
| 227 | } |
| 228 |