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