Last active
January 22, 2019 03:13
-
-
Save beckettkev/cbb4f1e594ef648e06b6287d6af39138 to your computer and use it in GitHub Desktop.
Uploading large files using the start, continue and finish upload functions with SharePoint 2013.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import utils from './utils'; | |
// First we need to make sure we are backwards compatible with IE (no ArrayBuffer.slice) | |
if (!ArrayBuffer.prototype.slice) { | |
ArrayBuffer.prototype.slice = function (begin, end) { | |
let len = this.byteLength; | |
begin = (begin|0) || 0; | |
end = end === (void 0) ? len : (end|0); | |
// Handle negative values. | |
if (begin < 0) begin = Math.max(begin + len, 0); | |
if (end < 0) end = Math.max(end + len, 0); | |
if (len === 0 || begin >= len || begin >= end) { | |
return new ArrayBuffer(0); | |
} | |
let length = Math.min(len - begin, end - begin); | |
let target = new ArrayBuffer(length); | |
let targetArray = new Uint8Array(target); | |
targetArray.set(new Uint8Array(this, begin, length)); | |
return target; | |
}; | |
} | |
function getWebRequestExecutorFactory(appWebUrl, hostWebUrl, spLanguage) { | |
let context = new window.SP.ClientContext(appWebUrl); | |
const factory = new window.SP.ProxyWebRequestExecutorFactory(appWebUrl); | |
context.set_webRequestExecutorFactory(factory); | |
return context; | |
} | |
const jsomContext = () => { | |
return getWebRequestExecutorFactory(utils.getSpContaxtUrlParams().appWebUrl, utils.getSpContaxtUrlParams().hostWebUrl, utils.getSpContaxtUrlParams().spLanguage); | |
}; | |
const jsomAppContext = () => { | |
let context = getWebRequestExecutorFactory(utils.getSpContaxtUrlParams().appWebUrl, utils.getSpContaxtUrlParams().hostWebUrl, utils.getSpContaxtUrlParams().spLanguage); | |
// Use the host web URL to get a parent context - this allows us to get data from the parent | |
let hostWebContext = new SP.AppContextSite(context, utils.getSpContaxtUrlParams().hostWebUrl); | |
return hostWebContext; | |
}; | |
// Base64 - this method converts the blob arrayBuffer into a binary string to send in the REST request | |
function convertDataBinaryString(data) { | |
let fileData = ''; | |
let byteArray = new Uint8Array(data); | |
for (var i = 0; i < byteArray.byteLength; i++) { | |
fileData += String.fromCharCode(byteArray[i]); | |
} | |
return fileData; | |
} | |
//this method sends the REST request using the SP RequestExecutor | |
function executeAsync(endPointUrl, data, requestHeaders) { | |
return new Promise((resolve, reject) => { | |
// using a utils function we would get the APP WEB url value and pass it into the constructor... | |
let executor = new SP.RequestExecutor(utils.getSpContaxtUrlParams().appWebUrl); | |
// Send the request. | |
executor.executeAsync({ | |
url: endPointUrl, | |
method: "POST", | |
body: data, | |
binaryStringRequestBody: true, | |
headers: requestHeaders, | |
success: offset => resolve(offset), | |
error: err => reject(err.responseText) | |
}); | |
}); | |
} | |
//this method sets up the REST request and then sends the chunk of file along with the unique indentifier (uploadId) | |
function uploadFileChunk(id, libraryPath, fileName, chunk, data, byteOffset) { | |
return new Promise((resolve, reject) => { | |
let offset = chunk.offset === 0 ? '' : ',fileOffset=' + byteOffset; | |
//parameterising the components of this endpoint avoids the max url length problem in SP (Querystring parameters are not included in this length) | |
let endpoint = String.format("{0}/_api/sp.appcontextsite(@target)/web/getfilebyserverrelativeurl(@libraryPath)/{5}(uploadId=guid'{3}'{6})?@target='{4}'&@libraryPath='/sites/assetdatabase/{1}/{2}'", | |
utils.getSpContaxtUrlParams().appWebUrl, libraryPath, fileName, id, utils.getSpContaxtUrlParams().hostWebUrl, chunk.method, offset); | |
const headers = { | |
"Accept": "application/json; odata=verbose", | |
"Content-Type": "application/octet-stream" | |
}; | |
executeAsync(endpoint, data, headers).then(offset => resolve(offset)).catch(err => reject(err.responseText)); | |
}); | |
} | |
/* | |
Calling this method is optional. If you have a need to show the progress, you can by updating an element in the DOM | |
with a changes progress indicator to update after each chunk has uploaded (shows the percentage progress of the upload). | |
*/ | |
function setLoaderMessage(uploading, percentage) { | |
let message = document.getElementsByClassName('loaderMessage'); | |
if (message !== null) { | |
message[0].innerHTML = uploading ? `Uploading file<br />${percentage}% complete.` : 'Working on it...'; | |
} | |
} | |
//sometimes the file gets checked out by the REST method, so we need to check it back in when we are done. | |
function checkinMajorVersion(libraryPath, fileName) { | |
return new Promise((resolve, reject) => { | |
let endpoint = String.format("{0}/_api/sp.appcontextsite(@target)/web/getfilebyserverrelativeurl(@libraryPath)/checkin(comment='File Added, initial commit.',checkintype=0)?@target='{3}'&@libraryPath='/sites/assetdatabase/{1}/{2}'", | |
utils.getSpContaxtUrlParams().appWebUrl, libraryPath, fileName, utils.getSpContaxtUrlParams().hostWebUrl); | |
const headers = { | |
"Accept": "application/json; odata=verbose" | |
}; | |
executeAsync(endpoint, '', headers).then(() => resolve(true)).catch(err => reject(err)); | |
}); | |
} | |
//the final REST call is made to get the file information after it has been fully uploaded (especially the file list item id) | |
function getFileInformation(libraryPath, fileName, resolve, reject) { | |
let endpoint = String.format("{0}/_api/sp.appcontextsite(@target)/web/getfilebyserverrelativeurl(@libraryPath)/ListItemAllFields?@target='{3}'&@libraryPath='/sites/assetdatabase/{1}/{2}'", | |
utils.getSpContaxtUrlParams().appWebUrl, libraryPath, fileName, utils.getSpContaxtUrlParams().hostWebUrl); | |
const headers = { | |
"Accept": "application/json; odata=verbose" | |
}; | |
executeAsync(endpoint, '', headers).then(fileListItem => { | |
console.log('fetching file information'); | |
const items = fileListItem.body ? fileListItem.body : fileListItem; | |
const listItem = JSON.parse(items); | |
//..and we are done. | |
resolve(listItem.d); | |
}).catch(err => { | |
reject(err.responseText); | |
}); | |
} | |
//the primary method that resursively calls to get the chunks and upload them to the library (to make the complete file) | |
function uploadFile(result, id, libraryPath, fileName, chunks, index, byteOffset, chunkPercentage, resolve, reject) { | |
//we slice the file blob into the chunk we need to send in this request (byteOffset tells us the start position) | |
const data = convertFileToBlobChunks(result, byteOffset, chunks[index]); | |
if (byteOffset === 0) { | |
//at the beginning of the upload set the message and starting percentage (0%) | |
setLoaderMessage(true, 0); | |
} | |
//upload the chunk to the server using REST, using the unique upload guid as the identifier | |
uploadFileChunk(id, libraryPath, fileName, chunks[index], data, byteOffset).then( | |
value => { | |
const isFinished = index === chunks.length - 1; | |
if (!isFinished) { | |
//the response value is a string of JSON (ugly) which we need to consume to find the offset | |
const response = typeof value.body !== 'undefined' ? JSON.parse(value.body) : ''; | |
//depending on the position in the upload, the response string (JSON) can differ! | |
if (typeof response.d.StartUpload !== 'undefined') { | |
byteOffset = Number.parseInvariant(response.d.StartUpload); | |
} else if (typeof response.d.ContinueUpload !== 'undefined') { | |
byteOffset = Number.parseInvariant(response.d.ContinueUpload); | |
} | |
} | |
index += 1; | |
const percentageComplete = isFinished ? 100 : Math.round((index * chunkPercentage)); | |
// progress indication | |
setLoaderMessage(true, percentageComplete); | |
console.log(percentageComplete + '%'); | |
//More chunks to process before the file is finished, continue | |
if (index < chunks.length) { | |
uploadFile(result, id, libraryPath, fileName, chunks, index, byteOffset, chunkPercentage, resolve, reject); | |
} else { | |
setLoaderMessage(false); | |
//check in the file and then resolve the file information back to the caller | |
//checkinMajorVersion(libraryPath, fileName).then(() => { | |
//when there was a checkin needed | |
getFileInformation(libraryPath, fileName, resolve, reject); | |
//}).catch(err => { | |
//no checkin was necessary | |
//getFileInformation(libraryPath, fileName, resolve, reject); | |
//}); | |
} | |
} | |
).catch(err => { console.log('Error in uploadFileChunk! '); window.Erz = err; }); | |
} | |
//this is the initial method we call to create a dummy place holder file before overwriting it with the chunks of data... | |
function createDummaryFile(ctx, fileName, libraryName) { | |
return new Promise((resolve, reject) => { | |
// Construct the endpoint - The GetList method is available for SharePoint Online only. | |
let endpoint = String.format("{0}/_api/sp.appcontextsite(@target)/web/lists/getByTitle('{1}')/rootfolder/files/add(overwrite=true, url='{2}')?@target='{3}'", | |
utils.getSpContaxtUrlParams().appWebUrl, libraryName, fileName, utils.getSpContaxtUrlParams().hostWebUrl); | |
const headers = { | |
"accept": "application/json;odata=verbose" | |
}; | |
executeAsync(endpoint, convertDataBinaryString(2), headers).then(file => resolve(true)).catch(err => reject(err.responseText)); | |
}); | |
} | |
//Helper method - depending on what chunk of data we are dealing with, we need to use the correct REST method... | |
function getUploadMethod(offset, length, total) { | |
if (offset + length + 1 > total) { | |
return 'finishupload'; | |
} else if (offset === 0) { | |
return 'startupload'; | |
} else if (offset < total) { | |
return 'continueupload'; | |
} | |
return null; | |
} | |
//this method slices the blob array buffer to the appropriate chunk and then calls off to get the BinaryString of that chunk | |
function convertFileToBlobChunks(result, byteOffset, chunkInfo) { | |
let arrayBuffer = chunkInfo.method === 'finishupload' ? result.slice(byteOffset) : result.slice(byteOffset, byteOffset + chunkInfo.length); | |
return convertDataBinaryString(arrayBuffer); | |
} | |
module.exports = { | |
upload: (file) => { | |
return new Promise((resolve, reject) => { | |
let ctx = jsomAppContext(); | |
// first we need to create a dummy file, before we can upload the file in chunks... | |
createDummaryFile(ctx, file.name, 'Documents').then(result => { | |
let fr = new FileReader(); | |
let offset = 0; | |
// the total file size in bytes... | |
let total = file.size; | |
// 1MB Chunks as represented in bytes (if the file is less than a MB, seperate it into two chunks of 80% and 20% the size)... | |
let length = 1000000 > total ? total * 0.8 : 1000000; | |
let chunks = []; | |
fr.onload = evt => { | |
while (offset < total) { | |
//if we are dealing with the final chunk, we need to know... | |
if (offset + length > total) { | |
length = total - offset; | |
} | |
//work out the chunks that need to be processed and the associated REST method (start, continue or finish) | |
chunks.push({ offset, length, method: getUploadMethod(offset, length, total) }); | |
offset += length; | |
} | |
//each chunk is worth a percentage of the total size of the file... | |
const chunkPercentage = parseFloat(((total / chunks.length) / total)) * 100; | |
if (chunks.length > 0) { | |
//the unique guid identifier to be used throughout the upload session | |
const id = utils.getGuid(); | |
//Start the upload - send the data to SP | |
uploadFile(evt.target.result, id, 'Documents', file.name, chunks, 0, 0, chunkPercentage, resolve, reject); | |
} | |
}; | |
//reads in the file using the fileReader HTML5 API (as an ArrayBuffer) - readAsBinaryString is not available in IE! | |
fr.readAsArrayBuffer(file); | |
}); | |
}); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment