Skip to content

Commit 8a766fb

Browse files
committed
feat: support chunk split max than 25m file
1 parent 67efafc commit 8a766fb

4 files changed

Lines changed: 82 additions & 11 deletions

File tree

src/files/fileCreate.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class FileCreate extends Endpoint {
7474
let duration: string = c.env.SHARE_DURATION
7575
let isEphemeral = false
7676
let isEncrypted = false
77-
let objectId = ''
77+
let objectId: string | Array<{ objectId: string }> = ''
7878
let hash = ''
7979
const contentType = c.req.header('Content-Type')
8080
if (
@@ -85,7 +85,7 @@ export class FileCreate extends Endpoint {
8585
const file = formData.get('file') as File
8686

8787
const fileInfo = this.getFormDataField<null | {
88-
objectId: string
88+
objectId: string | Array<{ objectId: string }>
8989
name: string
9090
type?: string
9191
size: number
@@ -116,7 +116,10 @@ export class FileCreate extends Endpoint {
116116
size = blob.size
117117
}
118118

119-
if ((!data || data.byteLength === 0) && !objectId) {
119+
if (
120+
(!data || data.byteLength === 0) &&
121+
(!objectId || (Array.isArray(objectId) && !objectId.length))
122+
) {
120123
return this.error('分享内容为空')
121124
}
122125

@@ -128,15 +131,22 @@ export class FileCreate extends Endpoint {
128131
}
129132

130133
const kv = this.getKV(c)
131-
const key = objectId || createId()
134+
const key = objectId && !Array.isArray(objectId) ? objectId : createId()
135+
// 直接上传
132136
if (data && data.byteLength) {
133137
await kv.put(key, data)
134138
hash = await sha256(data)
135-
} else {
139+
// 单个
140+
} else if (typeof objectId === 'string') {
136141
const cacheFile = await kv.get(objectId, 'stream')
137142
if (!cacheFile) {
138143
return this.error('分片上传的文件不存在')
139144
}
145+
// 分片存储
146+
} else if (Array.isArray(objectId) && objectId.length) {
147+
await kv.put(key, 'chunks', {
148+
metadata: objectId,
149+
})
140150
}
141151

142152
const db = this.getDB(c)

src/files/fileFetch.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ export class FileFetch extends Endpoint {
5050
}
5151
const objectId = record.objectId
5252

53-
const file = await kv.get(objectId, 'arrayBuffer')
53+
const {
54+
value: file,
55+
metadata,
56+
}: {
57+
value: null | ArrayBuffer
58+
metadata: null | Array<{
59+
objectId: string
60+
chunkId: number
61+
}>
62+
} = await kv.getWithMetadata(objectId, 'arrayBuffer')
5463

55-
if (!file) {
64+
if (!file && !metadata) {
5665
return new Response('Not found', {
5766
status: 404,
5867
headers: {
@@ -61,6 +70,31 @@ export class FileFetch extends Endpoint {
6170
})
6271
}
6372

73+
if (metadata) {
74+
// 大于 25M 不考虑文件类型
75+
const { readable, writable } = new TransformStream()
76+
const writer = writable.getWriter()
77+
;(async () => {
78+
for (let i = 0; i < metadata.length; i++) {
79+
const chunk = await kv.get(metadata[i].objectId, 'arrayBuffer')
80+
if (!chunk) {
81+
await writer.close()
82+
throw new Error('文件 Chunk 不完整')
83+
}
84+
await writer.write(new Uint8Array(chunk))
85+
}
86+
await writer.close()
87+
})().then()
88+
89+
return new Response(readable, {
90+
status: 200,
91+
headers: {
92+
'Content-Type': record.type ?? 'application/octet-stream',
93+
'Content-Disposition': `attachment; filename="${record.filename}"`,
94+
},
95+
})
96+
}
97+
6498
const isText = record.type === 'plain/string'
6599

66100
return new Response(file, {

src/scheduled.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { drizzle } from 'drizzle-orm/d1'
22
import { files } from '../data/schemas'
33
import { lte } from 'drizzle-orm'
44

5-
export async function scheduled(event: ScheduledEvent, env: Env) {
5+
export async function scheduled(_event: ScheduledEvent, env: Env) {
66
const db = drizzle(env.DB)
77
const kv = env.file_drops
88

@@ -16,7 +16,19 @@ export async function scheduled(event: ScheduledEvent, env: Env) {
1616
})
1717

1818
if (records.length) {
19-
await Promise.all(records.map((d) => kv.delete(d.objectId)))
19+
await Promise.all(
20+
records.map(async (d) => {
21+
const {
22+
value: _,
23+
metadata,
24+
}: { value: unknown; metadata: Array<{ objectId: string }> | null } =
25+
await kv.getWithMetadata(d.objectId, 'stream')
26+
if (Array.isArray(metadata) && metadata.length) {
27+
await Promise.all(metadata.map((d) => kv.delete(d.objectId)))
28+
}
29+
return kv.delete(d.objectId)
30+
}),
31+
)
2032
}
2133

2234
console.log(`clear before ${now}`)

web/api/uploader.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,23 @@ export class Uploader {
5555
const chunk = file.slice(start, end)
5656
tasks.push(this.uploadWithChunk(chunk))
5757
}
58-
const data = await Promise.all(tasks)
59-
console.log(data)
58+
const chunkInfo = (await Promise.all(tasks)).map((d, i) => ({
59+
...d,
60+
chunkId: i,
61+
}))
62+
formData.delete('file') // 移除 file
63+
formData.append(
64+
'fileInfo',
65+
JSON.stringify({
66+
objectId: chunkInfo,
67+
name: file.name,
68+
type: file.type,
69+
size: file.size,
70+
sha: await this.getSHA(file),
71+
}),
72+
)
73+
const { data } = await axios.put('/files', formData)
74+
return data as ApiResponseType<FileUploadedType>
6075
}
6176
throw new Error('建议使用 R2')
6277
}

0 commit comments

Comments
 (0)