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; let start = 0; let end = start + pieceSize; if (end > file.size) { end = file.size; } let chunks: {}[] = new Array(); while (start < totalSize) { 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}`, }, }; let res = await axios.put(uploadUrl, chunk["blob"], reqConfig); if (res.status === 202) { let { nextExpectedRanges } = res.data; start = parseInt(nextExpectedRanges[0].split("-")[0]); isUploadDone = false; } else if (res.status === 201) { resolve(res.data); } else if (res.status === 409) { 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