Skip to content

Commit ee82358

Browse files
committed
refac[frontend]: move upload logic to a separate file
1 parent 732c237 commit ee82358

File tree

4 files changed

+113
-82
lines changed

4 files changed

+113
-82
lines changed

frontend/components/PasteBin.tsx

Lines changed: 18 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,16 @@ import {
1616
maxExpirationReadable,
1717
BaseUrl,
1818
APIUrl,
19+
ErrorWithTitle,
20+
makeErrorMsg,
1921
} from "../utils/utils.js"
2022

2123
import "../style.css"
2224
import { UploadedPanel } from "./UploadedPanel.js"
2325
import { PasteEditor, PasteEditState } from "./PasteEditor.js"
24-
import { genKey, encodeKey, encrypt, EncryptionScheme } from "../utils/encryption.js"
25-
26-
async function genAndEncrypt(scheme: EncryptionScheme, content: string | Uint8Array) {
27-
const key = await genKey(scheme)
28-
const plaintext = typeof content === "string" ? new TextEncoder().encode(content) : content
29-
const ciphertext = await encrypt(scheme, key, plaintext)
30-
return { key: await encodeKey(key), ciphertext }
31-
}
26+
import { uploadPaste } from "../utils/uploader.js"
3227

3328
export function PasteBin() {
34-
const encryptionScheme: EncryptionScheme = "AES-GCM"
3529
const [editorState, setEditorState] = useState<PasteEditState>({
3630
editKind: "edit",
3731
editContent: "",
@@ -57,7 +51,7 @@ export function PasteBin() {
5751

5852
const [darkModeSelect, setDarkModeSelect] = useState<DarkMode>(defaultDarkMode())
5953

60-
function showModal(content: string, title: string) {
54+
function showModal(title: string, content: string) {
6155
setErrorState({ title, content, isOpen: true })
6256
}
6357

@@ -121,84 +115,31 @@ export function PasteBin() {
121115
}
122116
}, [])
123117

124-
async function uploadPaste(): Promise<void> {
125-
const fd = new FormData()
126-
if (editorState.editKind === "file") {
127-
if (editorState.file === null) {
128-
showModal("No file selected", "Error on preparing upload")
129-
return
130-
}
131-
if (pasteSetting.doEncrypt) {
132-
const { key, ciphertext } = await genAndEncrypt(encryptionScheme, await editorState.file.bytes())
133-
const file = new File([ciphertext], editorState.file.name)
134-
setUploadedEncryptionKey(key)
135-
fd.append("c", file)
136-
fd.append("encryption-scheme", encryptionScheme)
137-
} else {
138-
fd.append("c", editorState.file)
139-
}
140-
} else {
141-
if (editorState.editContent.length === 0) {
142-
showModal("Empty paste", "Error on preparing upload")
143-
return
144-
}
145-
if (pasteSetting.doEncrypt) {
146-
const { key, ciphertext } = await genAndEncrypt(encryptionScheme, editorState.editContent)
147-
setUploadedEncryptionKey(key)
148-
fd.append("c", new File([ciphertext], ""))
149-
fd.append("encryption-scheme", encryptionScheme)
150-
} else {
151-
fd.append("c", editorState.editContent)
152-
}
153-
}
154-
155-
fd.append("e", pasteSetting.expiration)
156-
if (pasteSetting.password.length > 0) fd.append("s", pasteSetting.password)
157-
158-
if (pasteSetting.uploadKind === "long") fd.append("p", "true")
159-
else if (pasteSetting.uploadKind === "custom") fd.append("n", pasteSetting.name)
160-
118+
async function onUploadPaste(): Promise<void> {
161119
try {
162-
setIsLoading(true)
163-
setPasteResponse(null)
164-
const isUpdate = pasteSetting.uploadKind !== "manage"
165-
// TODO: add progress indicator
166-
const resp = isUpdate
167-
? await fetch(APIUrl, {
168-
method: "POST",
169-
body: fd,
170-
})
171-
: await fetch(pasteSetting.manageUrl, {
172-
method: "PUT",
173-
body: fd,
174-
})
175-
if (resp.ok) {
176-
const respParsed = JSON.parse(await resp.text()) as PasteResponse
177-
setPasteResponse(respParsed)
178-
setIsLoading(false)
120+
const uploaded = await uploadPaste(pasteSetting, editorState, setUploadedEncryptionKey, setIsLoading)
121+
setPasteResponse(uploaded)
122+
} catch (error) {
123+
console.log(error)
124+
if (error instanceof ErrorWithTitle) {
125+
showModal(error.title, (error as Error).message)
179126
} else {
180-
await reportResponseError(resp, `Error ${resp.status}`)
181-
// will setIsLoading(false) on closing modal
127+
showModal("Error on Uploading Paste", (error as Error).message)
182128
}
183-
} catch (e) {
184-
showModal((e as Error).toString(), "Error on uploading paste")
185-
console.error(e)
186129
}
187130
}
188131

189132
async function deletePaste() {
190133
try {
191-
const resp = await fetch(pasteSetting.manageUrl, {
192-
method: "DELETE",
193-
})
134+
const resp = await fetch(pasteSetting.manageUrl, { method: "DELETE" })
194135
if (resp.ok) {
195-
showModal("It may takes 60 seconds for the deletion to propagate to the world", "Deletion succeeded")
136+
showModal("Deleted Successfully", "It may takes 60 seconds for the deletion to propagate to the world")
196137
setPasteResponse(null)
197138
} else {
198-
await reportResponseError(resp, `Error ${resp.status}`)
139+
showModal("Error From Server", await makeErrorMsg(resp))
199140
}
200141
} catch (e) {
201-
showModal((e as Error).message, "Error on deleting paste")
142+
showModal("Error on Deleting Paste", (e as Error).message)
202143
console.error(e)
203144
}
204145
}
@@ -250,7 +191,7 @@ export function PasteBin() {
250191
const submitter = (
251192
<div className="my-4 mx-2 lg:mx-0">
252193
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
253-
<Button color="primary" onPress={uploadPaste} className="mr-4" isDisabled={!canUpload() || isLoading}>
194+
<Button color="primary" onPress={onUploadPaste} className="mr-4" isDisabled={!canUpload() || isLoading}>
254195
{pasteSetting.uploadKind === "manage" ? "Update" : "Upload"}
255196
</Button>
256197
{pasteSetting.uploadKind === "manage" ? (
@@ -299,6 +240,7 @@ export function PasteBin() {
299240
/>
300241
{(pasteResponse || isLoading) && (
301242
<UploadedPanel
243+
isLoading={isLoading}
302244
pasteResponse={pasteResponse}
303245
encryptionKey={uploadedEncryptionKey}
304246
className="w-full lg:w-1/2"

frontend/components/UploadedPanel.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from "react"
33
import { PasteResponse } from "../../shared/interfaces.js"
44

55
interface UploadedPanelProps extends CardProps {
6+
isLoading: boolean
67
pasteResponse: PasteResponse | null
78
encryptionKey: string | null
89
}
@@ -13,7 +14,7 @@ const makeDecryptionUrl = (url: string, key: string) => {
1314
return urlParsed.toString() + "#" + key
1415
}
1516

16-
export function UploadedPanel({ pasteResponse, encryptionKey, ...rest }: UploadedPanelProps) {
17+
export function UploadedPanel({ isLoading, pasteResponse, encryptionKey, ...rest }: UploadedPanelProps) {
1718
const snippetClassNames = {
1819
pre: "overflow-scroll leading-[2.5] font-sans",
1920
base: "w-full py-1/3",
@@ -30,7 +31,7 @@ export function UploadedPanel({ pasteResponse, encryptionKey, ...rest }: Uploade
3031
<tr>
3132
<td className={firstColClassNames}>Paste URL</td>
3233
<td className="w-full">
33-
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl grow">
34+
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
3435
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
3536
{pasteResponse?.url}
3637
</Snippet>
@@ -40,7 +41,7 @@ export function UploadedPanel({ pasteResponse, encryptionKey, ...rest }: Uploade
4041
<tr>
4142
<td className={firstColClassNames}>Manage URL</td>
4243
<td className="w-full">
43-
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl grow">
44+
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
4445
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
4546
{pasteResponse?.manageUrl}
4647
</Snippet>
@@ -51,7 +52,7 @@ export function UploadedPanel({ pasteResponse, encryptionKey, ...rest }: Uploade
5152
<tr>
5253
<td className={firstColClassNames}>Decryption URL</td>
5354
<td className="w-full">
54-
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl grow">
55+
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
5556
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
5657
{pasteResponse && makeDecryptionUrl(pasteResponse.url, encryptionKey)}
5758
</Snippet>
@@ -62,7 +63,7 @@ export function UploadedPanel({ pasteResponse, encryptionKey, ...rest }: Uploade
6263
<tr>
6364
<td className={firstColClassNames}>Expire At</td>
6465
<td className="w-full py-2">
65-
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl">
66+
<Skeleton isLoaded={!isLoading} className="rounded-2xl">
6667
{pasteResponse && new Date(pasteResponse.expireAt).toLocaleString()}
6768
</Skeleton>
6869
</td>

frontend/utils/uploader.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,75 @@
1-
export async function uploadPaste() {}
1+
import { PasteSetting } from "../components/PasteSettingPanel.js"
2+
import { PasteEditState } from "../components/PasteEditor.js"
3+
import { APIUrl, ErrorWithTitle, makeErrorMsg } from "./utils.js"
4+
import { PasteResponse } from "../../shared/interfaces.js"
5+
import { encodeKey, encrypt, EncryptionScheme, genKey } from "./encryption.js"
6+
7+
async function genAndEncrypt(scheme: EncryptionScheme, content: string | Uint8Array) {
8+
const key = await genKey(scheme)
9+
const plaintext = typeof content === "string" ? new TextEncoder().encode(content) : content
10+
const ciphertext = await encrypt(scheme, key, plaintext)
11+
return { key: await encodeKey(key), ciphertext }
12+
}
13+
14+
const encryptionScheme: EncryptionScheme = "AES-GCM"
15+
16+
export async function uploadPaste(
17+
pasteSetting: PasteSetting,
18+
editorState: PasteEditState,
19+
onEncryptionKeyChange: (k: string) => void,
20+
onLoadingStateChange: (isLoading: boolean) => void,
21+
): Promise<PasteResponse> {
22+
const fd = new FormData()
23+
if (editorState.editKind === "file") {
24+
if (editorState.file === null) {
25+
throw new ErrorWithTitle("Error on Preparing Upload", "No file selected")
26+
}
27+
if (pasteSetting.doEncrypt) {
28+
const { key, ciphertext } = await genAndEncrypt(encryptionScheme, await editorState.file.bytes())
29+
const file = new File([ciphertext], editorState.file.name)
30+
onEncryptionKeyChange(key)
31+
fd.append("c", file)
32+
fd.append("encryption-scheme", encryptionScheme)
33+
} else {
34+
fd.append("c", editorState.file)
35+
}
36+
} else {
37+
if (editorState.editContent.length === 0) {
38+
throw new ErrorWithTitle("Error on Preparing Upload", "Empty paste")
39+
}
40+
if (pasteSetting.doEncrypt) {
41+
const { key, ciphertext } = await genAndEncrypt(encryptionScheme, editorState.editContent)
42+
onEncryptionKeyChange(key)
43+
fd.append("c", new File([ciphertext], ""))
44+
fd.append("encryption-scheme", encryptionScheme)
45+
} else {
46+
fd.append("c", editorState.editContent)
47+
}
48+
}
49+
50+
fd.append("e", pasteSetting.expiration)
51+
if (pasteSetting.password.length > 0) fd.append("s", pasteSetting.password)
52+
53+
if (pasteSetting.uploadKind === "long") fd.append("p", "true")
54+
else if (pasteSetting.uploadKind === "custom") fd.append("n", pasteSetting.name)
55+
56+
onLoadingStateChange(true)
57+
// setPasteResponse(null)
58+
const isUpdate = pasteSetting.uploadKind !== "manage"
59+
// TODO: add progress indicator
60+
const resp = isUpdate
61+
? await fetch(APIUrl, {
62+
method: "POST",
63+
body: fd,
64+
})
65+
: await fetch(pasteSetting.manageUrl, {
66+
method: "PUT",
67+
body: fd,
68+
})
69+
if (resp.ok) {
70+
onLoadingStateChange(false)
71+
return JSON.parse(await resp.text()) as PasteResponse
72+
} else {
73+
throw new ErrorWithTitle("Error From Server", await makeErrorMsg(resp))
74+
}
75+
}

frontend/utils/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ export const APIUrl = API_URL
77
export const maxExpirationSeconds = parseExpiration(MAX_EXPIRATION)!
88
export const maxExpirationReadable = parseExpirationReadable(MAX_EXPIRATION)!
99

10+
export class ErrorWithTitle extends Error {
11+
public title: string
12+
13+
constructor(title: string, msg: string) {
14+
super(msg)
15+
this.title = title
16+
}
17+
}
18+
19+
export async function makeErrorMsg(resp: Response): Promise<string> {
20+
const statusText = resp.statusText === "error" ? "Unknown error" : resp.statusText
21+
return (await resp.text()) || statusText
22+
}
23+
1024
export function formatSize(size: number): string {
1125
if (!size) return "0"
1226
if (size < 1024) {

0 commit comments

Comments
 (0)