简体   繁体   English

使用 Ajax 调用上传大文件

[英]Uploading Large Files with Ajax call

I am trying to use the Microsoft Graph API to upload large files using an Ajax call.我正在尝试使用 Microsoft Graph API 通过 Ajax 调用上传大文件。

According to the documentation , you first have to create an upload session which I can do successfully with my code.根据文档,您首先必须创建一个上传会话,我可以使用我的代码成功完成该会话。 The problem comes when I start my upload to the returned uploadUrl .当我开始上传到返回的uploadUrl时,问题就出现了。 I get the following error:我收到以下错误:

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

So when I check the actual request in Fiddler, I can see that the Content-Length header is set to 0 .因此,当我在 Fiddler 中检查实际请求时,可以看到Content-Length标头设置为0

So I tried setting my Content-Length header to the size of the ArrayBuffer that I'm sending, but I get an error (Chrome) that says:所以我尝试将我的Content-Length标头设置为我发送的ArrayBuffer的大小,但我收到一个错误(Chrome),内容为:

Refused to set unsafe header "Content-Length"

I've been struggling with this for 2 full days now and I'm at my wit's end.我已经为此苦苦挣扎了整整 2 天,但我的智慧已告一段落。 There is very little documentation on the Microsoft Graph API, and even fewer examples that seem to fit what I'm trying to do.关于 Microsoft Graph API 的文档很少,而且似乎适合我尝试做的事情的示例更少。

I can't imagine I'm the only one out there that's attempting to do this, I would think it would be a fairly common idea?我无法想象我是唯一一个尝试这样做的人,我认为这将是一个相当普遍的想法?

Below is the code I'm using.下面是我正在使用的代码。 I'm getting my AccessToken and URL elsewhere, but they seem to be fine as I can query using them from the console.我正在其他地方获取我的AccessTokenURL ,但它们似乎很好,因为我可以从控制台使用它们进行查询。

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 for returning the Array Buffer to send返回要发送的数组缓冲区的函数

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);
    };
};

And finally, the chunkUpload function:最后,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);
                }

I was able to figure out an answer to the problem, so I'll post the final code here as well for anyone else having an issue with this since there's very little examples I could find online to do this:我能够找出问题的答案,因此我也会在此处发布最终代码,供其他遇到此问题的人使用,因为我可以在网上找到很少的示例来执行此操作:

First of all, sending a bare ArrayBuffer in any browser (IE, Mozilla or Chrome) did not set the Content-Length to anything but 0, at least it wouldn't for me.首先,在任何浏览器(IE、Mozilla 或 Chrome)中发送一个裸 ArrayBuffer 并没有将 Content-Length 设置为 0 以外的任何值,至少对我来说不会。 If I converted the ArrayBuffer to a new uInt8Array, however, the browsers did pick up the Content-Length and set it correctly.但是,如果我将 ArrayBuffer 转换为新的 uInt8Array,浏览器确实会选择 Content-Length 并正确设置它。

Another Issue I found was in the Microsoft Graph documentation.我发现的另一个问题是在 Microsoft Graph 文档中。 I was not aware that you had to put the New File Name in the Upload Session Request URL - it's not clear that you need to do that in the documentation.我不知道您必须将新文件名放在上传会话请求 URL 中 - 不清楚您需要在文档中这样做。 See my code below, It's formatted correctly and works well.请参阅下面的代码,它的格式正确且运行良好。

Finally, my chunkedUpload function needed quite a few changes, most notably adjusting the xhr.send(file) to xhr.send(fileChunk) <--that was a big one I missed originally.最后,我的 chunkedUpload 函数需要进行很多更改,最显着的是将 xhr.send(file) 调整为 xhr.send(fileChunk) <--这是我最初错过的一个大改动。 I also included a Progress callback for the file upload that works very well with my bootstrap ProgressBar.我还为文件上传包含了一个 Progress 回调,它与我的引导程序 ProgressBar 配合得很好。

on to the working code:到工作代码:

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);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM