1. 获取UploadSession

在已经获取到AccessToken的条件下,发送post请求,获取uploadsession

1
2
3
4
5
6
7
8
9
10
POST https://graph.microsoft.com/v1.0/me/drive/root:/FolderA/FolderB/FileC.txt:/createUploadSession
Content-Type: application/json

{
"item": {
"@odata.type": "microsoft.graph.driveItemUploadableProperties",
"@microsoft.graph.conflictBehavior": "rename",
"name": "FileC.txt"
}
}

在这里,我们将文件FileC.txt上传到/FolderA/FolderB/路径下。rename,同名时将文件重命名。

看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
const requestUrl = `${driveApi}/root${encodePath(cleanPath)}/${filename}:/createUploadSession`
const reqConfig = {
headers: { 'Authorization': `Bearer ${accessToken}` ,'Content-Type': 'application/json'},
}
const reqData = JSON.stringify({
"item": {
"@odata.type": "microsoft.graph.driveItemUploadableProperties",
"@microsoft.graph.conflictBehavior": "rename",
"name": filename
}
})
const { data ,status } = await axios.post(requestUrl, reqData,reqConfig)

返回的data

1
2
3
4
5
6
7
8
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession",
    "expirationDateTime": "2022-12-12T20:59:36.57Z",
    "nextExpectedRanges": [
        "0-"
    ],
    "uploadUrl": "https://api.onedrive.com/rup/58719e3298535c9/eyJSZXNvdXJjZUlEIjoiNTg3MTlFMzI5ODUzNUM5ITE3ODU3MSIsIlJlbGF0aW9uc2hpcE5hbWUiOiJhYmMudHh0In0/4mZis-sabj24PL04uRIPxFu8jB1QNaqhHn3eEq-i__MpQWp4mF6nKvusl4Fox5Ft0yLsU_7QiHhK-NI--fVAOFYkn9UJNhSnhmm4RKzmdOYtQ/eyJuYW1lIjoiYWJjLnR4dCIsIkBuYW1lLmNvbmZsaWN0QmVoYXZpb3IiOiJyZW5hbWUifQ/4whWhy7zc-wazWcDSDucdkL6f5fEKVs3OonVBriho3EeaCue1gPmUTmqeHSsDsRaAvWjXqVN_337TlMGrEqJvIp-DK8aZsnw-1sB_fGObZ9cZAhBUT4O8Qh5EgWvr_GQLI9WBj94gFUEQxpTpHHGs6jpPcAJJJMHY1mB7iHzDbgsSuIjeDnMG7Cta7P0fGUWPVBXXYh6A9GdgEPkixGymS6LKbhbezFnDDoG4edG1IeOtqyYc372sEmERWDPdqkIdEbJ20LFKaGxGfposh4jkzTpWlNW8ga3KeiL8GwNJDZngOIER1NI-qJ1R_fFlU5yQTzM87zZCNUW52GGZT4k7qD7ghQzfrghgZJbxrVX2w1FNKXUhp3K9UXSNYfC6ITyboCPPOEd8iJ4Bp4Vgo5lCKTLHb9zG2-fmLun5swOTE1aB2Bt98KvOlho5uNMbArfM8bOrtUFAodtp_0ZeHoj6BPixKxKgkuxXwd3ArDpGLnVJpTnOaIvw3GnUClPM0IK6KPWufVf7f6iL9M7ih5LNMJ31u84ey9DHFXso-2LvMkC-lXbS7LrILZ-oRzvq19oZTSg3lvNczxJUuuRQqpDLkSA"
}

通过data.uploadUrl提取上传url。

2. 获取多个文件

1
2
3
4
<input type="file" ref={uploadInput} multiple className="hidden" onChange={handleFileEvent} />
<FloatButton tooltip={<div>{t('Upload files')}</div>} icon={<UploadOutlined />} onClick={() => {
uploadInput.current?.click()
}} />

在input标签中通过multiple指定文件可以为多个,并在onChange中指定文件选择完后的运行的函数,这里是handleFileEvent。ref用于找到input标签,我们将它隐藏,用一个按钮替代它,使它更加美观。

当文件选择完后,在handleFileEvent函数中获取并处理它。

1
2
3
4
const handleFileEvent = (e: { target: { files: any } }) => {
const chosenFiles = Array.prototype.slice.call(e.target.files)
}

e是input对象,我们使用 Array.prototype.slice.call(e.target.files)来获取多个文件,并将每个文件分开放入列表中,之后可以通过循环获取。

3. 切片

每个文件都很大,由于onedrive api指定我们每次上传的文件不能超过60MB且每块都必须是327680B的倍数,

所以我们首先需要将文件切块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sliceFile = (file: File, pieceSize = 5 * 256 * 256 * 30) => {
let totalSize = file.size; //total size of file
let start = 0; // start byte
let end = start + pieceSize; // end byte
if (end > file.size) {
end = file.size
}
let chunks: {}[] = new Array()
while (start < totalSize) {
// slice the length
// File inhert Blob, so it can use slice function.
let blob = file.slice(start, end);
chunks.push({ 'start': start, 'end': end - 1, 'blob': blob })
start = end;
end = start + pieceSize;
if (end > file.size) {
end = file.size
}
}
return chunks
}

上述将文件切块为5 * 256 * 256 * 30B大小的块,所有块放入chunks列表中。列表的每个元素是一个数组

{ ‘start’: start, ‘end’: end - 1, ‘blob’: blob },分别表示,开始的Byte,结束Byte,以及这些Byte的二进制内容。

4. 分段上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
async () => {
try {
let chunks = sliceFile(file, pieceSize)
while (getFileStatus(isUploadDone, file) === 'uploading') {
let chunk = chunks[start / pieceSize]
let reqConfig = {
headers: {
'Content-Range': `bytes ${chunk['start']}-${chunk['end']}/${file.size}`
}
}
//DO NOT use access token here.
let res = await axios.put(uploadUrl, chunk['blob'], reqConfig)
// res.status is 202, last chunk upload success
//get next range and redict
if (res.status === 202) {
let { nextExpectedRanges } = res.data
// "nextExpectedRanges": [
// "12345-55232",
// "77829-99375"
// ]
start = parseInt(nextExpectedRanges[0].split('-')[0])
isUploadDone = false
}
//all upload done
else if (res.status === 201) {
resolve(res.data)

}
//file conflict
else if (res.status === 409) {
//if error, upload url need to be delete from onedrive server
reject(file.name + ': file name conflict. You upload two file with same name')
}

}
} catch (err) {

reject(file.name + ':upload failed, msg:' + err)
}
}

上述代码省略了许多东西,我们主要是看它的功能实现,首先是利用sliceFile函数将文件切片,然后循环上传片段,由于onedrive在每次上传结束都会返回下次需要开始Byte的位置,而且不允许同时上传多个片段,所以我们需要等待上一个片段上传完毕,获取到下一个开始位置,再开始上传新的块。上传使用put,对应url为最开始我们获取的upload session。响应代码为202表示,没上传完,201表示上传结束,其他则为错误代码。

5. 暂停错误和断点续传

在上传过程中,可能遇到网络错误,或者主动暂停上传,这是我们可以在报错或者暂停逻辑中,保存上传的文件,并等待用户点击重新上传或继续上传(这两个逻辑对应相同处理方法)

对于报错后的处理,是将文件直接保存到一个js常量中

1
2
3
4
5
6
7
8
9
10
11
12

const pausedFiles = new Array<File>
const push2PausedFiles = (file: File) => {
let isSameFile = pausedFiles.some((f) => {
if (f.name === file.name) {
return true
}
})
if (!isSameFile) {
pausedFiles.push(file)
}
}

这里我们使用pausedFiles来储存暂停的文件

然后在用户点击重新上传时,通过get上传会话连接获取下一个开始的Byte点,并按照分段上传的逻辑,对文件切片,并从开始的byte点开始上传。

1
2
3
const rep = await axios.get(uploadUrl)
let { nextExpectedRanges } = rep.data
let start = parseInt(nextExpectedRanges[0].split('-')[0])

详细的代码逻辑在github/xieqifei/onedrive-vercel-manage

评论