简体   繁体   中英

Uploading Large Files with Ajax call

I am trying to use the Microsoft Graph API to upload large files using an Ajax call.

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 . 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 .

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:

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. There is very little documentation on the Microsoft Graph API, and even fewer examples that seem to fit what I'm trying to do.

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.

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:

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. If I converted the ArrayBuffer to a new uInt8Array, however, the browsers did pick up the Content-Length and set it correctly.

Another Issue I found was in the Microsoft Graph documentation. 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. 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. I also included a Progress callback for the file upload that works very well with my bootstrap 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);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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