繁体   English   中英

使用 Ajax 调用上传大文件

[英]Uploading Large Files with Ajax call

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

根据文档,您首先必须创建一个上传会话,我可以使用我的代码成功完成该会话。 当我开始上传到返回的uploadUrl时,问题就出现了。 我收到以下错误:

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

因此,当我在 Fiddler 中检查实际请求时,可以看到Content-Length标头设置为0

所以我尝试将我的Content-Length标头设置为我发送的ArrayBuffer的大小,但我收到一个错误(Chrome),内容为:

Refused to set unsafe header "Content-Length"

我已经为此苦苦挣扎了整整 2 天,但我的智慧已告一段落。 关于 Microsoft Graph API 的文档很少,而且似乎适合我尝试做的事情的示例更少。

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

下面是我正在使用的代码。 我正在其他地方获取我的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 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);
                }

我能够找出问题的答案,因此我也会在此处发布最终代码,供其他遇到此问题的人使用,因为我可以在网上找到很少的示例来执行此操作:

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

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

最后,我的 chunkedUpload 函数需要进行很多更改,最显着的是将 xhr.send(file) 调整为 xhr.send(fileChunk) <--这是我最初错过的一个大改动。 我还为文件上传包含了一个 Progress 回调,它与我的引导程序 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);
}

暂无
暂无

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

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