Skip to content

Commit 89e0332

Browse files
committed
feat: add backend support for multipart upload
1 parent 89479c9 commit 89e0332

File tree

13 files changed

+391
-79
lines changed

13 files changed

+391
-79
lines changed

frontend/components/DecryptPaste.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ export function DecryptPaste() {
4040
// const url = new URL("http://localhost:8787/d/dHYQ.jpg.txt#uqeULsBTb2I3iC7rD6AaYh4oJ5lMjJA2nYR+H0U8bEA=")
4141
const url = location
4242

43-
const { nameFromPath, ext, filename } = parsePath(url.pathname)
43+
const { name, ext, filename } = parsePath(url.pathname)
4444
const keyString = url.hash.slice(1)
4545

4646
useEffect(() => {
4747
if (keyString.length === 0) {
4848
showModal("No encryption key is given. You should append the key after a “#” character in the URL", "Error")
4949
}
50-
const pasteUrl = `${API_URL}/${nameFromPath}`
50+
const pasteUrl = `${API_URL}/${name}`
5151

5252
const fetchPaste = async () => {
5353
try {
@@ -82,7 +82,7 @@ export function DecryptPaste() {
8282
) || undefined
8383
: undefined
8484

85-
const inferredFilename = filename || (ext && nameFromPath + ext) || filenameFromDispTrimmed || nameFromPath
85+
const inferredFilename = filename || (ext && name + ext) || filenameFromDispTrimmed || name
8686
setPasteFile(new File([decrypted], inferredFilename))
8787
setPasteContentBuffer(decrypted)
8888
setFileBinary(isBinaryPath(inferredFilename))
@@ -159,7 +159,7 @@ export function DecryptPaste() {
159159
<span className="hidden md:inline">{INDEX_PAGE_TITLE}</span>
160160
</Link>
161161
<span className="mx-2">{" / "}</span>
162-
<code>{nameFromPath}</code>
162+
<code>{name}</code>
163163
<span className="ml-1">{isLoading ? " (Loading…)" : pasteFile ? " (Decrypted)" : ""}</span>
164164
</h1>
165165
{showFileContent && (

frontend/components/PasteBin.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ export function PasteBin() {
7070
useEffect(() => {
7171
// TODO: do not fetch paste for a large file paste
7272
const pathname = location.pathname
73-
const { nameFromPath, passwd, filename, ext } = parsePath(pathname)
73+
const { name, password, filename, ext } = parsePath(pathname)
7474

7575
const fetchPaste = async () => {
7676
try {
7777
setIsPasteLoading(true)
7878

79-
let pasteUrl = `${APIUrl}/${nameFromPath}`
79+
let pasteUrl = `${APIUrl}/${name}`
8080
if (filename) pasteUrl = `${pasteUrl}/${filename}`
8181
if (ext) pasteUrl = `${pasteUrl}${ext}`
8282

@@ -109,11 +109,11 @@ export function PasteBin() {
109109
setIsPasteLoading(false)
110110
}
111111
}
112-
if (passwd !== undefined && pasteSetting.manageUrl === "") {
112+
if (password !== undefined && pasteSetting.manageUrl === "") {
113113
setPasteSetting({
114114
...pasteSetting,
115115
uploadKind: "manage",
116-
manageUrl: `${APIUrl}/${nameFromPath}:${passwd}`,
116+
manageUrl: `${APIUrl}/${name}:${password}`,
117117
})
118118

119119
fetchPaste().catch(console.error)

frontend/utils/uploader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export async function uploadPaste() {}

src/handlers/handleDelete.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { parsePath } from "../shared.js"
44

55
export async function handleDelete(request: Request, env: Env, _: ExecutionContext) {
66
const url = new URL(request.url)
7-
const { nameFromPath, passwd } = parsePath(url.pathname)
8-
const metadata = await getPasteMetadata(env, nameFromPath)
7+
const { name, password } = parsePath(url.pathname)
8+
const metadata = await getPasteMetadata(env, name)
99
if (metadata === null) {
10-
throw new WorkerError(404, `paste of name '${nameFromPath}' not found`)
10+
throw new WorkerError(404, `paste of name '${name}' not found`)
1111
} else {
12-
if (passwd !== metadata.passwd) {
13-
throw new WorkerError(403, `incorrect password for paste '${nameFromPath}`)
12+
if (password !== metadata.passwd) {
13+
throw new WorkerError(403, `incorrect password for paste '${name}`)
1414
} else {
15-
await deletePaste(env, nameFromPath, metadata)
15+
await deletePaste(env, name, metadata)
1616
return new Response("the paste will be deleted in seconds")
1717
}
1818
}

src/handlers/handleMPU.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { MPUCreateResponse, NAME_REGEX, PASTE_NAME_LEN, PRIVATE_PASTE_NAME_LEN } from "../shared.js"
2+
import { genRandStr, WorkerError } from "../common.js"
3+
import { getPasteMetadata, pasteNameAvailable } from "../storage/storage.js"
4+
5+
// POST /mpu/create?n=<optional n>&p=<optional isPrivate>
6+
// returns JSON { name: string, key: string, uploadId: string }
7+
export async function handleMPUCreate(request: Request, env: Env): Promise<Response> {
8+
const url = new URL(request.url)
9+
const n = url.searchParams.get("n")
10+
const isPrivate = url.searchParams.get("p") !== null
11+
12+
let name: string | undefined
13+
if (n) {
14+
if (!NAME_REGEX.test(n)) {
15+
throw new WorkerError(400, `illegal paste name ‘${n}’ for MPU create`)
16+
}
17+
name = "~" + n
18+
if (!(await pasteNameAvailable(env, n))) {
19+
throw new WorkerError(409, `name ‘${name}’ is already used`)
20+
}
21+
} else {
22+
name = genRandStr(isPrivate ? PRIVATE_PASTE_NAME_LEN : PASTE_NAME_LEN)
23+
}
24+
25+
const multipartUpload = await env.R2.createMultipartUpload(name)
26+
const resp: MPUCreateResponse = {
27+
name,
28+
key: multipartUpload.key,
29+
uploadId: multipartUpload.uploadId,
30+
}
31+
return new Response(JSON.stringify(resp))
32+
}
33+
34+
// POST /mpu/create-update?name=<name>&password=<password>
35+
// returns JSON { name: string, key: string, uploadId: string }
36+
export async function handleMPUCreateUpdate(request: Request, env: Env): Promise<Response> {
37+
const url = new URL(request.url)
38+
const name = url.searchParams.get("name")
39+
const password = url.searchParams.get("password")
40+
if (name === null || password === null) {
41+
throw new WorkerError(400, `missing name or password (password) in searchParams`)
42+
}
43+
44+
const metadata = await getPasteMetadata(env, name)
45+
if (metadata === null) {
46+
throw new WorkerError(404, `paste of name ‘${name}’ is not found`)
47+
}
48+
if (password !== metadata.passwd) {
49+
throw new WorkerError(403, `incorrect password for paste ‘${name}’`)
50+
}
51+
52+
const multipartUpload = await env.R2.createMultipartUpload(name)
53+
const resp: MPUCreateResponse = {
54+
name,
55+
key: multipartUpload.key,
56+
uploadId: multipartUpload.uploadId,
57+
}
58+
return new Response(JSON.stringify(resp))
59+
}
60+
61+
// PUT /mpu/resume?key=<key>&uploadId=<uploadId>&partNumber=<partNumber>
62+
// return JSON { partNumber: number, etag: string }
63+
export async function handleMPUResume(request: Request, env: Env): Promise<Response> {
64+
const url = new URL(request.url)
65+
66+
const uploadId = url.searchParams.get("uploadId")
67+
const partNumberString = url.searchParams.get("partNumber")
68+
const key = url.searchParams.get("key")
69+
if (partNumberString === null || uploadId === null || key === null) {
70+
throw new WorkerError(400, "missing partNumber or uploadId or key in searchParams")
71+
}
72+
if (request.body === null) {
73+
throw new WorkerError(400, "missing request body")
74+
}
75+
76+
const partNumber = parseInt(partNumberString)
77+
const multipartUpload = env.R2.resumeMultipartUpload(key, uploadId)
78+
const uploadedPart: R2UploadedPart = await multipartUpload.uploadPart(partNumber, request.body)
79+
return new Response(JSON.stringify(uploadedPart))
80+
}
81+
82+
// POST /mpu/complete?name=<name>&key=<key>&uploadId=<uploadId>
83+
// formdata same as POST/PUT a normal paste, but
84+
// - field `c` is interpreted as JSON { partNumber: number, etag: string }[]
85+
// - field `n` is ignored
86+
export async function handleMPUComplete(request: Request, env: Env, completeBody: R2UploadedPart[]): Promise<string> {
87+
const url = new URL(request.url)
88+
const uploadId = url.searchParams.get("uploadId")
89+
const key = url.searchParams.get("key")
90+
const name = url.searchParams.get("name")
91+
if (uploadId === null || key === null || name === null) {
92+
throw new WorkerError(400, `no uploadId or key for MPU complete`)
93+
}
94+
95+
const multipartUpload = env.R2.resumeMultipartUpload(key, uploadId)
96+
97+
const object = await multipartUpload.complete(completeBody)
98+
if (name !== object.key) {
99+
throw new WorkerError(400, `name ‘${name}’ is not consistent with the originally specified name`)
100+
}
101+
return object.httpEtag
102+
}

src/handlers/handleRead.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export async function handleGet(request: Request, env: Env, ctx: ExecutionContex
107107

108108
const url = new URL(request.url)
109109

110-
const { role, nameFromPath, ext, filename } = parsePath(url.pathname)
110+
const { role, name, ext, filename } = parsePath(url.pathname)
111111

112112
const disp = url.searchParams.has("a") ? "attachment" : "inline"
113113

@@ -116,12 +116,12 @@ export async function handleGet(request: Request, env: Env, ctx: ExecutionContex
116116
const shouldGetPasteContent = (!isHead && role !== "m" && role !== "d") || (isHead && role === "u")
117117

118118
const item: PasteWithMetadata | null = shouldGetPasteContent
119-
? await getPaste(env, nameFromPath, ctx)
120-
: await getPasteWithoutContent(env, nameFromPath)
119+
? await getPaste(env, name, ctx)
120+
: await getPasteWithoutContent(env, name)
121121

122122
// when paste is not found
123123
if (item === null) {
124-
throw new WorkerError(404, `paste of name '${nameFromPath}' not found`)
124+
throw new WorkerError(404, `paste of name '${name}' not found`)
125125
}
126126

127127
let inferred_mime =
@@ -204,7 +204,7 @@ export async function handleGet(request: Request, env: Env, ctx: ExecutionContex
204204
pageUrl.pathname = "/decrypt.html"
205205
const page = decode(await (await env.ASSETS.fetch(pageUrl)).arrayBuffer()).replace(
206206
"{{PASTE_NAME}}",
207-
nameFromPath + (filename ? "/" + filename : ext ? ext : ""),
207+
name + (filename ? "/" + filename : ext ? ext : ""),
208208
)
209209
return new Response(isHead ? null : page, {
210210
headers: {

0 commit comments

Comments
 (0)