Skip to content

Commit e845903

Browse files
Y-ASLantKirCute
andauthored
feat(onedrive): support frontend direct upload (#249)
* Fix Bugs. - 上传路径Bug修复 - 减少API请求次数 - 将“获取前端上传信息“函数设计为一个驱动实现的接口驱动未实现接口或调用返回errs.NotImplement表示驱动不支持(未启用)前端上传,支持以下两种情况: > 未实现接口:storage.(driver.DirectUploader) 类型断言失败 → 不支持 > 返回 NotImplement:接口已实现但功能未启用(如 OneDrive 的 EnableDirectUpload=false)→ 不支持 * Fix分享界面复制直连无法使用问题 * refactor --------- Co-authored-by: KirCute <951206789@qq.com>
1 parent 5e1146e commit e845903

File tree

9 files changed

+209
-19
lines changed

9 files changed

+209
-19
lines changed

src/hooks/usePath.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export const usePath = () => {
177177
ObjStore.setHeader(data.header)
178178
ObjStore.setWrite(data.write)
179179
ObjStore.setProvider(data.provider)
180+
ObjStore.setDirectUploadTools(data.direct_upload_tools)
180181
ObjStore.setState(State.Folder)
181182
},
182183
handleErr,

src/lang/en/drivers.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,8 @@
779779
"custom_host": "Custom host",
780780
"custom_host-tips": "Custom host for onedrive download link",
781781
"disable_disk_usage": "Disable disk usage",
782+
"enable_direct_upload": "Enable direct upload",
783+
"enable_direct_upload-tips": "Enable direct upload from client to OneDrive",
782784
"is_sharepoint": "Is sharepoint",
783785
"redirect_uri": "Redirect uri",
784786
"refresh_token": "Refresh token",

src/pages/home/uploads/Upload.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,27 @@ const Upload = () => {
103103
const setUpload = (path: string, key: keyof UploadFileProps, value: any) => {
104104
setUploadFiles("uploads", (upload) => upload.path === path, key, value)
105105
}
106+
107+
// All upload methods are available by default
106108
const uploaders = getUploads()
107109
const [curUploader, setCurUploader] = createSignal(uploaders[0])
108110
const handleFile = async (file: File) => {
109111
const path = file.webkitRelativePath ? file.webkitRelativePath : file.name
110112
setUpload(path, "status", "uploading")
111113
const uploadPath = pathJoin(pathname(), path)
112114
try {
113-
const err = await curUploader().upload(
114-
uploadPath,
115-
file,
116-
(key, value) => {
117-
setUpload(path, key, value)
118-
},
119-
uploadConfig.asTask,
120-
uploadConfig.overwrite,
121-
uploadConfig.rapid,
122-
)
115+
const err = await curUploader()
116+
.upload(
117+
uploadPath,
118+
file,
119+
(key, value) => {
120+
setUpload(path, key, value)
121+
},
122+
uploadConfig.asTask,
123+
uploadConfig.overwrite,
124+
uploadConfig.rapid,
125+
)
126+
.catch((err) => err)
123127
if (!err) {
124128
setUpload(path, "status", "success")
125129
setUpload(path, "progress", 100)
@@ -243,7 +247,7 @@ const Upload = () => {
243247
</Heading>
244248
<Box w={{ "@initial": "80%", "@md": "30%" }}>
245249
<SelectWrapper
246-
value={curUploader().name}
250+
value={curUploader()?.name}
247251
onChange={(name) => {
248252
setCurUploader(
249253
uploaders.find((uploader) => uploader.name === name)!,

src/pages/home/uploads/direct.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Upload, SetUpload } from "./types"
2+
import { r, pathDir } from "~/utils"
3+
4+
export const HttpDirectUpload: Upload = async (
5+
uploadPath: string,
6+
file: File,
7+
setUpload: SetUpload,
8+
_asTask: boolean,
9+
overwrite: boolean,
10+
_rapid: boolean,
11+
) => {
12+
const path = pathDir(uploadPath)
13+
14+
// Get direct upload info from backend
15+
const resp = await r.post(
16+
"/fs/get_direct_upload_info",
17+
{
18+
path,
19+
file_name: file.name,
20+
file_size: file.size,
21+
tool: "HttpDirect",
22+
},
23+
{
24+
headers: {
25+
Overwrite: overwrite,
26+
},
27+
},
28+
)
29+
30+
const uploadInfo = resp.data
31+
32+
// If upload_info is null, direct upload is not supported - fallback to Stream
33+
if (!uploadInfo) {
34+
throw new Error("Http Direct Upload not supported")
35+
}
36+
37+
// Upload file directly to storage
38+
const chunkSize = uploadInfo.chunk_size || 0
39+
const uploadURL = uploadInfo.upload_url
40+
const method = uploadInfo.method || "PUT"
41+
42+
if (chunkSize > 0 && file.size > chunkSize) {
43+
// Chunked upload
44+
return await uploadChunked(
45+
file,
46+
uploadURL,
47+
chunkSize,
48+
method,
49+
uploadInfo.headers,
50+
setUpload,
51+
)
52+
} else {
53+
// Single upload
54+
return await uploadSingle(
55+
file,
56+
uploadURL,
57+
method,
58+
uploadInfo.headers,
59+
setUpload,
60+
)
61+
}
62+
}
63+
64+
async function uploadSingle(
65+
file: File,
66+
uploadURL: string,
67+
method: string,
68+
headers?: Record<string, string>,
69+
setUpload?: SetUpload,
70+
): Promise<undefined> {
71+
const xhr = new XMLHttpRequest()
72+
73+
return new Promise((resolve, reject) => {
74+
xhr.upload.addEventListener("progress", (e) => {
75+
if (e.lengthComputable && setUpload) {
76+
const progress = (e.loaded / e.total) * 100
77+
setUpload("progress", progress)
78+
setUpload("speed", 0) // Speed calculation not implemented
79+
}
80+
})
81+
82+
xhr.addEventListener("load", () => {
83+
if (xhr.status >= 200 && xhr.status < 300) {
84+
resolve(undefined)
85+
} else {
86+
reject(new Error(`Upload failed with status ${xhr.status}`))
87+
}
88+
})
89+
90+
xhr.addEventListener("error", () => {
91+
reject(new Error("Upload failed"))
92+
})
93+
94+
xhr.open(method, uploadURL)
95+
96+
// Set custom headers if provided
97+
if (headers) {
98+
Object.entries(headers).forEach(([key, value]) => {
99+
xhr.setRequestHeader(key, value)
100+
})
101+
}
102+
103+
xhr.send(file)
104+
})
105+
}
106+
107+
async function uploadChunked(
108+
file: File,
109+
uploadURL: string,
110+
chunkSize: number,
111+
method: string,
112+
headers?: Record<string, string>,
113+
setUpload?: SetUpload,
114+
): Promise<undefined> {
115+
const totalChunks = Math.ceil(file.size / chunkSize)
116+
let uploadedBytes = 0
117+
118+
for (let i = 0; i < totalChunks; i++) {
119+
const start = i * chunkSize
120+
const end = Math.min(start + chunkSize, file.size)
121+
const chunk = file.slice(start, end)
122+
123+
const xhr = new XMLHttpRequest()
124+
125+
await new Promise<void>((resolve, reject) => {
126+
xhr.upload.addEventListener("progress", (e) => {
127+
if (e.lengthComputable && setUpload) {
128+
const chunkProgress = uploadedBytes + e.loaded
129+
const progress = (chunkProgress / file.size) * 100
130+
setUpload("progress", progress)
131+
setUpload("speed", 0) // Speed calculation not implemented
132+
}
133+
})
134+
135+
xhr.addEventListener("load", () => {
136+
if (xhr.status >= 200 && xhr.status < 300) {
137+
uploadedBytes += chunk.size
138+
resolve()
139+
} else {
140+
reject(
141+
new Error(`Upload chunk ${i + 1} failed with status ${xhr.status}`),
142+
)
143+
}
144+
})
145+
146+
xhr.addEventListener("error", () => {
147+
reject(new Error(`Upload chunk ${i + 1} failed`))
148+
})
149+
150+
xhr.open(method, uploadURL)
151+
152+
// Set Content-Range header for chunked upload
153+
xhr.setRequestHeader(
154+
"Content-Range",
155+
`bytes ${start}-${end - 1}/${file.size}`,
156+
)
157+
158+
// Set custom headers if provided
159+
if (headers) {
160+
Object.entries(headers).forEach(([key, value]) => {
161+
xhr.setRequestHeader(key, value)
162+
})
163+
}
164+
165+
xhr.send(chunk)
166+
})
167+
}
168+
169+
return undefined
170+
}

src/pages/home/uploads/form.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const FormUpload: Upload = async (
1010
asTask = false,
1111
overwrite = false,
1212
rapid = false,
13-
): Promise<Error | undefined> => {
13+
): Promise<undefined> => {
1414
let oldTimestamp = new Date().valueOf()
1515
let oldLoaded = 0
1616
const form = new FormData()
@@ -60,6 +60,6 @@ export const FormUpload: Upload = async (
6060
if (resp.code === 200) {
6161
return
6262
} else {
63-
return new Error(resp.message)
63+
throw new Error(resp.message)
6464
}
6565
}

src/pages/home/uploads/stream.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const StreamUpload: Upload = async (
1010
asTask = false,
1111
overwrite = false,
1212
rapid = false,
13-
): Promise<Error | undefined> => {
13+
): Promise<undefined> => {
1414
let oldTimestamp = new Date().valueOf()
1515
let oldLoaded = 0
1616
let headers: { [k: string]: any } = {
@@ -58,6 +58,6 @@ export const StreamUpload: Upload = async (
5858
if (resp.code === 200) {
5959
return
6060
} else {
61-
return new Error(resp.message)
61+
throw new Error(resp.message)
6262
}
6363
}

src/pages/home/uploads/uploads.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
import { objStore } from "~/store"
22
import { FormUpload } from "./form"
33
import { StreamUpload } from "./stream"
4+
import { HttpDirectUpload } from "./direct"
45
import { Upload } from "./types"
56

67
type Uploader = {
78
upload: Upload
89
name: string
9-
provider: RegExp
10+
available: () => boolean
1011
}
1112

13+
// All upload methods
1214
const AllUploads: Uploader[] = [
15+
{
16+
name: "HTTP Direct",
17+
upload: HttpDirectUpload,
18+
available: () => {
19+
return objStore.direct_upload_tools?.includes("HttpDirect") || false
20+
},
21+
},
1322
{
1423
name: "Stream",
1524
upload: StreamUpload,
16-
provider: /.*/,
25+
available: () => true,
1726
},
1827
{
1928
name: "Form",
2029
upload: FormUpload,
21-
provider: /.*/,
30+
available: () => true,
2231
},
2332
]
2433

2534
export const getUploads = (): Pick<Uploader, "name" | "upload">[] => {
26-
return AllUploads.filter((u) => u.provider.test(objStore.provider))
35+
return AllUploads.filter((u) => u.available())
2736
}

src/store/obj.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const initialObjStore = {
2828
readme: "",
2929
header: "",
3030
provider: "",
31+
direct_upload_tools: <string[] | undefined>undefined,
3132
state: State.Initial,
3233
err: "",
3334
}
@@ -76,6 +77,8 @@ export const ObjStore = {
7677
// setObjStore("write", resp.data.write);
7778
// },
7879
setState: (state: State) => setObjStore("state", state),
80+
setDirectUploadTools: (tools?: string[]) =>
81+
setObjStore("direct_upload_tools", tools),
7982
setErr: (err: string) => setObjStore("err", err),
8083
}
8184

src/types/resp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type FsListResp = Resp<{
1818
header: string
1919
write: boolean
2020
provider: string
21+
direct_upload_tools?: string[]
2122
}>
2223

2324
export type SearchNode = {

0 commit comments

Comments
 (0)