1. 获取 UploadSession

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

1
2
3
4
5
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
14
15
16
17
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
{
"@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
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
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

评论