2017-09-26 230 views
1

我正在嘗試使用Microsoft Graph API使用Ajax調用來上傳大文件。使用Ajax調用上傳大文件

根據documentation,你首先必須創建一個上傳會話,我可以成功地使用我的代碼。當我開始上傳到返回的uploadUrl時,問題就出現了。我得到以下錯誤:

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

所以,當我檢查提琴手的實際要求,我可以看到Content-Length頭被設置爲0

所以,我想我的Content-Length頭設置到我發送ArrayBuffer的大小,但我得到一個錯誤(鉻),上面寫着:

Refused to set unsafe header "Content-Length"

我一直在爲此而努力現在整整兩天,而且我在智慧的結尾。 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); 
       } 
+0

貴'PUT'要求有一個'轉移,Encoding'頭?如果是這樣,那麼價值是什麼? – Brad

+0

不,我沒有Transfer-Encoding標頭 - 我從MS Graph API文檔中沒有看到它的必要條件。我應該有嗎? –

+0

不,但我想確保分塊編碼沒有出現在圖片中。瀏覽器應該正確地設置'Content-Length'頭部,所以它是0的事實很有趣。 – Brad

回答

0

我能找出一個答案的問題,所以我我會在這裏發佈最終代碼以及其他任何與此有關的問題,因爲我可以在網上找到很少的示例來執行此操作:

首先,在任何瀏覽器(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); 
}