代码之家  ›  专栏  ›  技术社区  ›  Chris Bloom

使用Ajax调用上载大文件

  •  1
  • Chris Bloom  · 技术社区  · 9 年前

    我正在尝试使用Microsoft Graph API使用Ajax调用上载大文件。

    根据 documentation uploadUrl . 我得到以下错误:

    {
        code: "invalidRequest",
        message: "The Content-Range header length does not match the provided number of bytes."
    }
    

    Content-Length 标题设置为 0 .

    所以我试着设置 标题的大小 ArrayBuffer

    Refused to set unsafe header "Content-Length"

    我已经为此挣扎了整整两天了,我已经智穷了。关于Microsoft Graph API的文档很少,适合我尝试的示例更少。

    我无法想象我是唯一一个尝试这样做的人,我认为这是一个相当普遍的想法?

    下面是我正在使用的代码。我正在拿我的 AccessToken URL 在其他地方,但它们似乎很好,因为我可以从控制台使用它们进行查询。

    this.UploadLargeFileToFolderID = function (FolderID,
        FileObject,
        ShowLoadingMessage,
        SuccessFunction,
        ErrorFunction,
        CompleteFunction) { //will upload a file up to 60Mb to folder.
    
        //shows the loading messag
        ShowLoadingMessage && ThisRepository.LoadingMessage.Show();
    
        //cleans the file name to something acceptable to SharePoint
        FileObject.name = CleanOneDriveFileName(FileObject.name);
    
        var UploadSessionURL = FolderID ?
            ThisRepository.RepositoryRootURL + '/drive/items/' + FolderID + '/createUploadSession' :
            ThisRepository.RepositoryRootURL + '/drive/root/createUploadSession';
    
        //First, get the Upload Sesion.
        $.ajax({
            url: UploadSessionURL,
            method: 'POST',
            headers: {
                authorization: "Bearer " + ThisRepository.AccessToken
            },
            success: function (data, textStatus, jqXHR) {
                //successfully got the upload session.            
                console.log('Session:');
                console.log(data);
    
                //Create the ArrayBuffer and upload the file.
                ReturnArrayBufferFromFile(FileObject, function (ArrayBuffer) {
                    console.log('Array Buffer:');
                    console.log(ArrayBuffer);
                    var MaxChunkSize = 327680;
                    var ChunkSize = ArrayBuffer.byteLength < MaxChunkSize ?
                        ArrayBuffer.byteLength :
                        MaxChunkSize;
    
                    chunkedUpload(data.uploadUrl, ArrayBuffer, ChunkSize, 0, null,
                        null, null, null,
                        function (response) {
                            console.log(response);
                            !SuccessFunction && console.log(response);
                            typeof SuccessFunction === 'function' && SuccessFunction(response);
                        });
    
                });
    
            },
            error: function (jqXHR, textStatus, errorThrown) {
                console.log(jqXHR);
                typeof ErrorFunction === 'function' && ErrorFunction(jqXHR);
            },
            complete: function (jqXHR, textStatus) {
                ThisRepository.LoadingMessage.Remove();
                typeof CompleteFunction === 'function' && CompleteFunction(jqXHR);
            },
        });
    
    };
    

    用于返回要发送的数组缓冲区的函数

    function ReturnArrayBufferFromFile(InputFile, CallBackFunction) {
        console.log('Input File');
        console.log(InputFile);
        var FileName = CleanOneDriveFileName(InputFile.name);
        var FileUploadReader = new FileReader();
    
        if (InputFile.type.match('image.*')) {
            // if the file is an image, we want to make sure 
            // it's not too big before we return it.
            FileUploadReader.onloadend = function (e) {
                var img = new Image();
    
                //will resize an image to a maximum of 2 megapixels.
                img.onload = function () {
                    var MAX_HEIGHT = 2048; //max final height, in pixels
                    var MAX_WIDTH = 2048; //max final width, in pixels
                    var height = img.height;
                    var width = img.width;
    
                    //do the resizing
                    if (width > height) { //landscape image
                        if (width > MAX_WIDTH) {
                            height *= MAX_WIDTH / width;
                            width = MAX_WIDTH;
                        };
                    } else { //portrait image
                        if (height > MAX_HEIGHT) {
                            width *= MAX_HEIGHT / height;
                            height = MAX_HEIGHT;
                        };
                    };
    
                    //Create a new canvas element, correctly sized with the image
                    var canvas = document.createElement("canvas");
                    canvas.width = width;
                    canvas.height = height;
                    canvas.getContext('2d').drawImage(this, 0, 0, width, height);
    
                    //Create the new file reader for the upload function.                   
                    var ConvertedFile = canvas.toBlob(function (blob) {
                        var ConvertedFileReader = new FileReader();
    
                        ConvertedFileReader.onloadend = function (loadendevent) {
                            //return loadendevent.target.result;
                            var result = loadendevent.target.result;
                            var Rawresult = result.split(',')[1];
                            CallBackFunction(loadendevent.target.result);
                        };
    
                        ConvertedFileReader.readAsArrayBuffer(blob);
    
                    }, 'image/jpeg', 0.90);
                };
    
                img.src = e.target.result;
            };
    
            FileUploadReader.readAsArrayBuffer(InputFile);
        } else {
            //File is not an image.  No pre-work is required.  Just upload it.
            FileUploadReader.onloadend = function (e) {
                CallBackFunction(e.target.result);
            };
            FileUploadReader.readAsArrayBuffer(InputFile);
        };
    };
    

    最后,chunkUpload函数:

    function chunkedUpload(url, file, chunkSize, chunkStart,
        chunkEnd, chunks, chunksDone, fileChunk, CompleteCallBack) {
    
        var filesize = file.byteLength;
    
        chunkSize = chunkSize ? chunkSize : 327680;
        chunkStart = chunkStart ? chunkStart : 0;
        chunkEnd = chunkEnd ? chunkEnd : chunkSize;
        chunks = chunks ? chunks : filesize / chunkSize;
        chunksDone = chunksDone ? chunksDone : 0;
        fileChunk = fileChunk ? fileChunk : file.slice(chunkStart, chunkEnd);
    
        var req = new XMLHttpRequest();
    
        req.open("PUT", url, true);
        //req.setRequestHeader("Content-Length", file.size.toString());
        req.setRequestHeader("Content-Range", "bytes " + chunkStart + "-" +
            (chunkEnd - 1) + "/" + filesize);
    
        req.onload = (e) => {
                let response = JSON.parse(req.response);
                console.log(response);
                if (response.nextExpectedRanges) {
                    let range = response.nextExpectedRanges[0].split('-'),
                        chunkStart = Number(range[0]),
                        nextChunk = chunkStart + chunkSize,
                        chunkEnd = nextChunk > file.byteLength ? file.byteLength : nextChunk;
                    console.log(((chunksDone++/ chunks) * 100), '%' );
                                chunkedUpload(url, file, chunkSize, chunkStart,
                                    chunkEnd, chunks, chunksDone, CompleteCallBack);
                            }
                            else {
                                console.log("upload session complete");
                                typeof CompleteCallBack === 'function' &&
                                    CompleteCallBack(response);
                            }
                        }; req.send(file);
                    }
    
    1 回复  |  直到 9 年前
        1
  •  0
  •   Chris Bloom    9 年前

    我已经找到了这个问题的答案,所以我将在这里发布最终代码,供其他对此有问题的人使用,因为我在网上找到的例子很少:

    首先,在任何浏览器(如Mozilla或Chrome)中发送一个裸ArrayBuffer都不会将内容长度设置为0以外的任何值,至少对我来说不会。 但是,如果我将ArrayBuffer转换为一个新的uInt8Array,浏览器确实会选择内容长度并正确设置。

    我发现的另一个问题是在Microsoft Graph文档中。我不知道你必须在上传会话请求URL中输入新文件名-不清楚你是否需要在文档中这样做。请参阅下面的代码,它的格式正确,运行良好。

    最后,我的chunkedUpload函数需要做很多更改,最显著的是调整了xhr。将(文件)发送到xhr。发送(fileChunk)<--这是我最初错过的一个重要时刻。我还包括了一个用于文件上传的进度回调,该回调与我的引导程序ProgressBar配合得很好。

    关于工作代码:

    this.UploadLargeFileToFolderID = function (FolderID, FileObject, ShowLoadingMessage, SuccessFunction, ErrorFunction, CompleteFunction, ProgressFunction) {//will upload a file up to 60Mb to folder.
        ShowLoadingMessage && ThisRepository.LoadingMessage.Show(); //shows the loading message
    
        FileObject.name = CleanOneDriveFileName(FileObject.name); //cleans the file name to something acceptable to SharePoint
        var NewFileName = CleanOneDriveFileName(FileObject.name);
        var UploadSessionURL = FolderID ? ThisRepository.RepositoryRootURL + '/drive/items/' + FolderID + ':/' + NewFileName + ':/createUploadSession' : ThisRepository.RepositoryRootURL + '/drive/root:/' + NewFileName + ':/createUploadSession';
        var PathToParent = FolderID ? ThisRepository.RepositoryRootURL + '/drive/items/' + FolderID + ':/' + NewFileName + ':/' : ThisRepository.RepositoryRootURL + '/drive/root:/' + NewFileName + ':/'; //used if we have a naming conflict and must rename the object.
    
        var UploadSessionOptions = {
            item: {
                //"@microsoft.graph.conflictBehavior": "rename",
                "@microsoft.graph.conflictBehavior": "replace",
            }
        };
    
        //First, get the Upload Sesion.
        $.ajax({
            url: UploadSessionURL,
            method: 'POST',
            headers: { authorization: "Bearer " + ThisRepository.AccessToken, 'Content-Type': 'application/json', 'accept': 'application/json'},
            data: JSON.stringify(UploadSessionOptions),
            success: function (SessionData, textStatus, jqXHR) { //successfully got the upload session.
                //Create the ArrayBuffer and upload the file.
                ReturnArrayBufferFromFile(FileObject,
                        function (ArrayBuffer) {
                        var uInt8Array = new Uint8Array(ArrayBuffer);
                        var FileSize = uInt8Array.length;
                        var MaxChunkSize = 3276800; //approx 3.2Mb.  Microsoft Graph OneDrive API says that all chunks MUST be in a multiple of 320Kib (327,680 bytes).  Recommended is 5Mb-10Mb for good internet connections.  
                        var ChunkSize = FileSize < MaxChunkSize ? FileSize : MaxChunkSize;
                        var DataUploadURL = SessionData.uploadUrl;
    
                        chunkedUpload(DataUploadURL, uInt8Array, ChunkSize, 0, null, null, null,
                            function (progress) { //progress handler
                                ProgressFunction(progress);
                            },
                            function (response) { //completion handler
                        if (response.StatusCode == 201 || response.StatusCode == 200) {//success.  201 is 'Created' and 200 is 'OK'
    
                            typeof SuccessFunction === 'function' && SuccessFunction(response);
    
                            ThisRepository.LoadingMessage.Remove();
                            typeof CompleteFunction === 'function' && CompleteFunction(response);
    
                        } else if (response.StatusCode == 409) { //naming conflict?
    
                            //if we had a renaming conflict error, per Graph Documentation we can make a simple PUT request to rename the file
    
                            //HAVE NOT SUCCESSFULLY TESTED THIS...
                            var NewDriveItemResolve = {
                                "name": NewFileName,
                                "@microsoft.graph.conflictBehavior": "rename",
                                "@microsoft.graph.sourceUrl": DataUploadURL
                            };
    
                            $.ajax({
                                url: PathToParent,
                                method: "PUT",
                                headers: { authorization: "Bearer " + ThisRepository.AccessToken, 'Content-Type': 'application/json', accept: 'application/json' },
                                data: JSON.stringify(NewDriveItemResolve),
                                success: function (RenameSuccess) {
                                    console.log(RenameSuccess);
                                    typeof SuccessFunction === 'function' && SuccessFunction(response);
    
                                    ThisRepository.LoadingMessage.Remove();
                                    typeof CompleteFunction === 'function' && CompleteFunction(response);
    
                                },
                                error: function (RenameError) {
                                    console.log(RenameError);
    
                                    var CompleteObject = { StatusCode: RenameError.status, ResponseObject: RenameError, Code: RenameError.error ? RenameError.error.code : 'Unknown Error Code', Message: RenameError.error ? RenameError.error.message : 'Unknown Error Message' };
                                    var Status = CompleteObject.StatusCode;
                                    var StatusText = CompleteObject.Code;
                                    var ErrorMessage = CompleteObject.Message;
    
                                    var ErrorMessage = new Alert({ Location: ThisRepository.LoadingMessage.Location, Type: 'danger', Text: "Status: " + Status + ': ' + StatusText + "<br />Error: " + ErrorMessage + '<br />Rest Endpoint: ' + data.uploadUrl });
                                    ErrorMessage.ShowWithoutTimeout();
    
                                    typeof ErrorFunction == 'function' && ErrorFunction(response);
    
                                    ThisRepository.LoadingMessage.Remove();
                                    typeof CompleteFunction === 'function' && CompleteFunction(response);
    
                                },
                                complete: function (RenameComplete) { /* Complete Function */ }
    
                            });
    
    
                        } else { //we had an error of some kind.
    
                            var Status = response.StatusCode;
                            var StatusText = response.Code;
                            var ErrorMessage = response.Message;
    
                            var ErrorMessage = new Alert({ Location: ThisRepository.LoadingMessage.Location, Type: 'danger', Text: "Status: " + Status + ': ' + StatusText + "<br />Error: " + ErrorMessage + '<br />Rest Endpoint: ' + data.uploadUrl });
                            ErrorMessage.ShowWithoutTimeout();
    
                            //CANCEL THE UPLOAD SESSION.
                            $.ajax({
                                url: UploadSessionURL,
                                method: "DELETE",
                                headers: { authorization: "Bearer " + ThisRepository.AccessToken },
                                success: function (SessionCancelled) { console.log('Upload Session Cancelled');},
                                error: function (SessionCancelError) { /* Error Goes Here*/},
                            });
    
                            typeof ErrorFunction == 'function' && ErrorFunction(response);
    
                            ThisRepository.LoadingMessage.Remove();
                            typeof CompleteFunction === 'function' && CompleteFunction(response);
                        };                      
                    });
                    }
                );
            },
            error: function (jqXHR, textStatus, errorThrown) {
                console.log('Error Creating Session:');
                console.log(jqXHR);
                //WE MAY HAVE A CANCELLED UPLOAD...TRY TO DELETE THE OLD UPLOAD SESSION, TELL THE USER TO TRY AGAIN.
                //COULD OPTIONALLY RUN A "RENAME" ATTEMPT HERE AS WELL
                $.ajax({
                    url: PathToParent,
                    method: "DELETE",
                    headers: { authorization: "Bearer " + ThisRepository.AccessToken },
                    success: function (SessionCancelled) { console.log('Upload Session Cancelled'); },
                    error: function (SessionCancelError) { console.log(SessionCancelError); },
                });
    
                typeof ErrorFunction === 'function' && ErrorFunction(jqXHR);
            },
            complete: function (jqXHR, textStatus) { /* COMPLETE CODE GOES HERE */},
        });   
    };
    
    function ReturnArrayBufferFromFile(InputFile, CallBackFunction) {
    var FileName = InputFile.name;
    var FileUploadReader = new FileReader();
    
    //Check the file type.  If it's an image, we want to make sure the user isn't uploading a very high quality image (2 megapixel max for our purposes).
    
    if (InputFile.type.match('image.*')) { // it's an image, so we will resize it before returning the array buffer...
        FileUploadReader.onloadend = function (e) {
            var img = new Image();
    
            img.onload = function () { //will resize an image to a maximum of 2 megapixels.
    
                var MAX_HEIGHT = 2048;//max final height, in pixels
                var MAX_WIDTH = 2048; //max final width, in pixels
                var height = img.height;
                var width = img.width;
    
                //do the resizing
                if (width > height) {//landscape image
                    if (width > MAX_WIDTH) {
                        height *= MAX_WIDTH / width;
                        width = MAX_WIDTH;
                    };
                }
                else { //portrait image
                    if (height > MAX_HEIGHT) {
                        width *= MAX_HEIGHT / height;
                        height = MAX_HEIGHT;
                    };
                };
    
                //Create a new canvas element, correctly sized with the image
                var canvas = document.createElement("canvas");
                canvas.width = width;
                canvas.height = height;
                canvas.getContext('2d').drawImage(this, 0, 0, width, height);
    
                //Create the new file reader for the upload function.                   
                var ConvertedFile = canvas.toBlob(function (blob) {
                    var ConvertedFileReader = new FileReader();
    
                    ConvertedFileReader.onloadend = function (loadendevent) { //return the ArrayBuffer
                        CallBackFunction(loadendevent.target.result);
                    };
    
                    ConvertedFileReader.readAsArrayBuffer(blob);
                    //ConvertedFileReader.readAsDataURL(blob);
    
    
                }, 'image/jpeg', 0.90);
            };
    
            img.src = e.target.result;
        };
    
        FileUploadReader.readAsDataURL(InputFile);
    }
    else {
        FileUploadReader.onloadend = function (e) {//File is not an image.  No pre-work is required.  Just return as an array buffer.
            CallBackFunction(e.target.result);
        };
        FileUploadReader.readAsArrayBuffer(InputFile);
    };
    };
    
    function chunkedUpload(url, file, chunkSize, chunkStart, chunkEnd, chunks, 
        chunksDone, ProgressCallBack, CompleteCallBack) {
    
    var filesize = file.length;
    
    
    chunkSize = chunkSize ? chunkSize : 3276800; //note:  Microsoft Graph indicates all chunks MUST be in a multiple of 320Kib (327,680 bytes).  
    chunkStart = chunkStart ? chunkStart : 0;
    chunkEnd = chunkEnd ? chunkEnd : chunkSize;
    chunks = chunks ? chunks : Math.ceil(filesize / chunkSize);
    chunksDone = chunksDone ? chunksDone : 0;
    console.log('NOW CHUNKS DONE = ' + chunksDone);
    fileChunk = file.slice(chunkStart, chunkEnd);
    
    var TotalLoaded = chunksDone * chunkSize;
    
    var req = new XMLHttpRequest();
    
    req.upload.addEventListener('progress', function (progressobject) {
        var ThisProgress = progressobject.loaded ? progressobject.loaded : 0;
        var OverallPctComplete = parseFloat((TotalLoaded + ThisProgress) / filesize);
        ProgressCallBack({ PercentComplete: OverallPctComplete });
    }, false);
    
    req.open("PUT", url, true);
    req.setRequestHeader("Content-Range", "bytes " + chunkStart + "-" + (chunkEnd - 1) + "/" + filesize);
    
    req.onload = function (e) {
        var response = JSON.parse(req.response);
        var Status = req.status;
        var CallBackObject = {
            StatusCode: Status,
            ResponseObject: req,
        };
    
        if (Status == 202) { //response ready for another chunk.
            var range = response.nextExpectedRanges[0].split('-'),
                chunkStart = Number(range[0]),
                nextChunk = chunkStart + chunkSize,
                chunkEnd = nextChunk > filesize ? filesize : nextChunk;
    
            chunksDone++;
            TotalLoaded = chunksDone * chunkSize;
    
            CallBackObject.Code = "Accepted",
            CallBackObject.Message = "Upload Another Chunk";
    
            chunkedUpload(url, file, chunkSize, chunkStart, chunkEnd, chunks, chunksDone++, ProgressCallBack, CompleteCallBack);
    
        } else {//we are done
            if (Status == 201 || Status == 200) {//successfully created or uploaded
                CallBackObject.Code = 'Success';
                CallBackObject.Message = 'File was Uploaded Successfully.';
            } else { //we had an error.
                var ErrorCode = response.error ? response.error.code : 'Unknown Error Code';
                var ErrorMessage = response.error ? response.error.message : 'Unknown Error Message';
    
                CallBackObject.Code = ErrorCode;
                CallBackObject.Message = ErrorMessage;
            };
            CompleteCallBack(CallBackObject);
        };
    
    
    };
    
    req.send(fileChunk);
    }