Autodesk Forge and 416 (Requested Range Not Satisfiable) - upload

I'm trying to send a Revit file to my Bucket chunk by chunk. My Revit file is almost 13 MB. Here is my code:
function handleFileSelect(evt) {
var files = evt.target.files;
var file = files[0];
var segmentSize = 1024 * 1024 * 5; //5 MB
var startingByte = 0;
var endingByte = startingByte + segmentSize - 1;
var segments = Math.ceil(file.size / segmentSize);
var session = Math.floor(100000000 + Math.random() * -900000000);
for (var i = 0; i < segments; i ++)
{
var blob = file.slice(startingByte, endingByte);
var url = 'https://developer.api.autodesk.com/oss/v2/buckets/' + 'linked_model' + '/objects/' + file.name + '/resumable';
//console.log(url);
var contentRange = 'bytes ' + startingByte + '-' + endingByte + '/' + file.size;
$.ajax({
type: 'PUT',
url: url,
data: blob,
headers: {
'Authorization':'Bearer ' + token,
'Content-Type':'application/octet-stream',
'Content-Range': contentRange,
'Session-Id': session
},
crossDomain: true,
processData: false,
success: function (data) {
console.log(i);
startingByte = endingByte + 1;
endingByte = startingByte + segmentSize - 1;
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("Status: " + textStatus); alert("Error: " + errorThrown);
console.log(startingByte);
console.log(endingByte);
console.log(file.size);
}
});
}
}
It gives me error: 416 (Requested Range Not Satisfiable)
Can anyone help?

I had the same 416 error but my issue was that I tried to upload chunks smaller than 2MB, which is not doable (except for the last chunk).
When I increased the chunks size to 5MB it started to work. I just wrote a blog article about it: https://forge.autodesk.com/blog/nailing-large-files-uploads-forge-resumable-api
Below is the core piece of code that handles chunking and uploading (in node.js).
By the way, I strongly discourage you to perform this kind of operation client-side as your snippet suggests, this means you have to pass a write-access token to the web page which compromises security of your app. You should first upload the file to your server and then securely upload it to Forge as described in the post and my sample.
/////////////////////////////////////////////////////////
// Uploads object to bucket using resumable endpoint
//
/////////////////////////////////////////////////////////
uploadObjectChunked (getToken, bucketKey, objectKey,
file, opts = {}) {
return new Promise((resolve, reject) => {
const chunkSize = opts.chunkSize || 5 * 1024 * 1024
const nbChunks = Math.ceil(file.size / chunkSize)
const chunksMap = Array.from({
length: nbChunks
}, (e, i) => i)
// generates uniques session ID
const sessionId = this.guid()
// prepare the upload tasks
const uploadTasks = chunksMap.map((chunkIdx) => {
const start = chunkIdx * chunkSize
const end = Math.min(
file.size, (chunkIdx + 1) * chunkSize) - 1
const range = `bytes ${start}-${end}/${file.size}`
const length = end - start + 1
const readStream =
fs.createReadStream(file.path, {
start, end: end
})
const run = async () => {
const token = await getToken()
return this._objectsAPI.uploadChunk(
bucketKey, objectKey,
length, range, sessionId,
readStream, {},
{autoRefresh: false}, token)
}
return {
chunkIndex: chunkIdx,
run
}
})
let progress = 0
// runs asynchronously in parallel the upload tasks
// number of simultaneous uploads is defined by
// opts.concurrentUploads
eachLimit(uploadTasks, opts.concurrentUploads || 3,
(task, callback) => {
task.run().then((res) => {
if (opts.onProgress) {
progress += 100.0 / nbChunks
opts.onProgress ({
progress: Math.round(progress * 100) / 100,
chunkIndex: task.chunkIndex
})
}
callback ()
}, (err) => {
console.log('error')
console.log(err)
callback(err)
})
}, (err) => {
if (err) {
return reject(err)
}
return resolve({
fileSize: file.size,
bucketKey,
objectKey,
nbChunks
})
})
})
}

Related

Making A Cloudflare Worker That Tweets Though My Twitter Developer APIs. Can't Get CryptoJS.HmacSHA1 to Create Signature for Fetch Request

I am making a Cloudflare Worker that Tweets from my Twitter Developer APIs whenever I make a blank GET request to the worker. At first you might think "that's easy just use npm's twitter-api-v2," but that won't work because Cloudflare workers need to be in pure javascript/typescript and can't rely on any external modules. So I attempted to do this with the following code.
index.ts
import { handleRequest } from './handler'
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
handler.ts
import * as CryptoJS from 'crypto-js'
function getRandomString(length) {
const randomChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for ( let i = 0; i < length; i++ ) {
result += randomChars.charAt(Math.floor(Math.random() * randomChars.length));
}
return result;
}
function hexToBase64(str) {
const stringChange = str.toString()
return btoa(String.fromCharCode.apply(null, stringChange.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
}
async function postTweet() {
const oauth_consumer_key = '<OAUTH CONSUMER KEY HERE>'
const oauth_consumer_secret = '<OAUTH CONSUMER SECRET HERE>'
const oauth_token = '<OAUTH TOKEN HERE>'
const oauth_token_secret = '<OAUTH TOKEN SECRET HERE>'
const oauth_signature_method = 'HMAC-SHA1'
const oauth_version = '1.0'
const oauth_nonce = getRandomString(42)
const oauth_timestamp = Math.round(Date.now() / 1000)
const endpointURL = 'https://api.twitter.com/1.1/statuses/update.json?status';
const tweetData = {
status: 'I am Tweeting this now'
}
const encodedTweet = encodeURIComponent(tweetData.status).replace(/!/g, '%21')
const params = 'POST&' + encodeURIComponent('https://api.twitter.com/1.1/statuses/update.json') + '&include_entities' + encodeURIComponent('=true&') + 'oauth_consumer_key' + encodeURIComponent('='+oauth_consumer_key+'&') + 'oauth_nonce' + encodeURIComponent('='+oauth_nonce+'&') + 'oauth_signature_method' + encodeURIComponent('='+oauth_signature_method+'&') + 'oauth_timestamp' + encodeURIComponent('='+oauth_timestamp+'&') + 'oauth_token' + encodeURIComponent('='+oauth_token+'&') + 'oauth_version' + encodeURIComponent('='+oauth_version+'&') + 'status' + encodeURIComponent('='+encodedTweet)
const signingKey = encodeURIComponent(oauth_consumer_secret) + '&' + encodeURIComponent(oauth_token_secret)
//////HMAC-SHA1 Functionality//////
const hexStr = CryptoJS.HmacSHA1(params, signingKey)
console.log(hexStr)
const signature = hexToBase64(hexStr)
const oauth_signature = encodeURIComponent(signature)
fetch('https://api.twitter.com/1.1/statuses/update.json', {
method: 'post',
headers: {
'Authorization': 'OAuth oauth_consumer_key="'+oauth_consumer_key+'",oauth_token="'+oauth_token+'",oauth_signature_method="HMAC-SHA1",oauth_timestamp="'+oauth_timestamp+'",oauth_nonce="'+oauth_nonce+'",oauth_version="1.0",oauth_signature="'+oauth_signature+'"',
'Content-Type': 'application/x-www-form-urlencoded' // 'application/json'
},
body: JSON.stringify(tweetData)
}).then(function(response) {
return response.json();
}).then(function(data) {
console.log('result:', data);
});
console.log('postTweet ran')
}
export async function handleRequest(request: Request): Promise<Response> {
await postTweet()
return new Response(`Hello worker! this is a ${request.method} request`, {
headers: { 'content-type': 'text/plain' },
});
}
But when I run the code with wrangler dev and then do a blank GET request to http://127.0.0.1:8787 with Postman, I get this in my terminal:
<myusername>#<mycomputername> <myappname> % wrangler dev
👂 Listening on http://127.0.0.1:8787
🌀 Detected changes...
💁 Ignoring stale first change
[2022-04-24 15:42:37] GET <myappname>.<myworkername>.workers.dev/ HTTP/1.1 200 OK
{unknown object}
postTweet ran
^C
<myusername>#<mycomputername> <myappname> %
I noticed that the problem probably starts with the fact that const hexStr = CryptoJS.HmacSHA1(params, signingKey) is failing. You can see in the output that console.log(hexStr) is printing {unknown object}
What am I doing wrong? And how can I get my Cloudflare worker to Tweet upon request?
that won't work because Cloudflare workers need to be in pure javascript/typescript and can't rely on any external modules
That's not the case, Node.js support was announced recently: https://blog.cloudflare.com/node-js-support-cloudflare-workers/
There's also a list of libraries (from npm) that work with Workers: https://workers.cloudflare.com/works

How to fix NativeScript file upload on some files aborts the upload after just 196608 bytes

On some files, (ios) at the moment is the only platform I'm testing on, if the file is under 30 megs everything seems to work using nativescript-background-http to upload a video file to the server. If the filesize is over 30 megs the upload aborts at exactly 196608 bytes every time.
I know it's not a server issue aborting the upload. As I have set both the POST Size Limits in IIS and the Max File Size Limit in ColdFusion to 500 megs.
mediafilepicker.on("getFiles", function (res) {
let results = res.object.get('results');
if (results) {
for (let i = 0; i < results.length; i++) {
let result = results[i];
let file = result.file;
if (result.file && applicationModule.ios)
{
console.dir(result);
// We can copy it to app directory, if need
//let fileName = "myTmpImage.mov";
var session = bghttp.session("image-upload");
var sFile = result.file.replace("file://", "");
var filename = sFile.substring(sFile.lastIndexOf('/') + 1);
var mimetype = filename.substring(filename.lastIndexOf('.') + 1);
console.log(sFile);
var request = {
url: 'http://www.mobilecoach.me/test_upload.cfm',
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"File-Name" : filename
},
description: "Uploading"
};
var params = [
{ name: "file", filename: sFile, mimeType: "video/" + mimetype }
];
task = session.multipartUpload(params, request);
task.on("progress", logEvent);
task.on("error", logEvent);
task.on("complete", logEvent);
function logEvent(e) {
console.log("currentBytes: " + e.currentBytes);
console.log("totalBytes: " + e.totalBytes);
console.log(e.eventName);
if(e.eventName == "complete")
{
dialogsModule.alert({
message: "Upload Completed",
okButtonText: "Ok",
cancelable: false
}).then(() => {
})
}
}
}
}
}
});
Like I said on less than 30 meg files works fine. Over 30 megs upload aborts without error just completes/aborts # exactly 196608 bytes.

Netsuite OAuth Not Working

I've tried implementing Netsuite's OAuth Example, as illustrated here: https://netsuite.custhelp.com/app/answers/detail/a_id/42165. I've posted it directly below so you don't have to go to the page if you don't want.
Unfortunately, it's not working. I know that I have the correct token and consumer key's and secrets, and the correct account ID. It's giving me a nice error though:
{"error" : {"code" : "INVALID_LOGIN_ATTEMPT", "message" : "Invalid login attempt."}}
I can look in my login audits, and see that it's saying that the signature is invalid. But the code itself looks fine and was provided by Netsuite.
I've also tried some approaches in Node.JS and haven't gotten them working. Any suggestions as to which direction I should go next?
import oauth2 as oauth
import requests
import time
url = "https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1"
token = oauth.Token(key="080eefeb395df81902e18305540a97b5b3524b251772adf769f06e6f0d9dfde5", secret="451f28d17127a3dd427898c6b75546d30b5bd8c8d7e73e23028c497221196ae2")
consumer = oauth.Consumer(key="504ee7703e1871f22180441563ad9f01f3f18d67ecda580b0fae764ed7c4fd38", secret="b36d202caf62f889fbd8c306e633a5a1105c3767ba8fc15f2c8246c5f11e500c")
http_method = "GET"
realm="ACCT123456"
params = {
'oauth_version': "1.0",
'oauth_nonce': oauth.generate_nonce(),
'oauth_timestamp': str(int(time.time())),
'oauth_token': token.key,
'oauth_consumer_key': consumer.key
}
req = oauth.Request(method=http_method, url=url, parameters=params)
signature_method = oauth.SignatureMethod_HMAC_SHA1()
req.sign_request(signature_method, consumer, token)
header = req.to_header(realm)
headery = header['Authorization'].encode('ascii', 'ignore')
headerx = {"Authorization": headery, "Content-Type":"application/json"}
print(headerx)
conn = requests.get("https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1",headers=headerx)
print(conn.text)
I've also implemented a few Node.JS samples (that haven't worked either). Here is one of them (CryptoJS HMAC-SHA1 and HMAC-SHA256 are on top, then the oauth-1.0a library, and then the code provided by Netsuite with a few small changes to make it work (added a hash_function, renamed 'public' to 'key'):
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(g,l){var e={},d=e.lib={},m=function(){},k=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
p=d.WordArray=k.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=l?c:4*a.length},toString:function(a){return(a||n).stringify(this)},concat:function(a){var c=this.words,q=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b<a;b++)c[f+b>>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535<q.length)for(b=0;b<a;b+=4)c[f+b>>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=k.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b<a;b+=4)c.push(4294967296*g.random()|0);return new p.init(c,a)}}),b=e.enc={},n=b.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f<a;f++){var d=c[f>>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f<c;f+=2)b[f>>>3]|=parseInt(a.substr(f,
2),16)<<24-4*(f%8);return new p.init(b,c/2)}},j=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f<a;f++)b.push(String.fromCharCode(c[f>>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f<c;f++)b[f>>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new p.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(j.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return j.parse(unescape(encodeURIComponent(a)))}},
r=d.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,f=c.sigBytes,d=this.blockSize,e=f/(4*d),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*d;f=g.min(4*a,f);if(a){for(var k=0;k<a;k+=d)this._doProcessBlock(b,k);k=b.splice(0,a);c.sigBytes-=f}return new p.init(k,f)},clone:function(){var a=k.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});d.Hasher=r.extend({cfg:k.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){r.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new s.HMAC.init(a,
d)).finalize(b)}}});var s=e.algo={};return e}(Math);
(function(){var g=CryptoJS,l=g.lib,e=l.WordArray,d=l.Hasher,m=[],l=g.algo.SHA1=d.extend({_doReset:function(){this._hash=new e.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(d,e){for(var b=this._hash.words,n=b[0],j=b[1],h=b[2],g=b[3],l=b[4],a=0;80>a;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(n<<5|n>>>27)+l+m[a];c=20>a?c+((j&h|~j&g)+1518500249):40>a?c+((j^h^g)+1859775393):60>a?c+((j&h|j&g|h&g)-1894007588):c+((j^h^
g)-899497514);l=g;g=h;h=j<<30|j>>>2;j=n;n=c}b[0]=b[0]+n|0;b[1]=b[1]+j|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+l|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,g=8*d.sigBytes;e[g>>>5]|=128<<24-g%32;e[(g+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(g+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(l);g.HmacSHA1=d._createHmacHelper(l)})();
(function(){var g=CryptoJS,l=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=l.parse(d));var g=e.blockSize,k=4*g;d.sigBytes>k&&(d=e.finalize(d));d.clamp();for(var p=this._oKey=d.clone(),b=this._iKey=d.clone(),n=p.words,j=b.words,h=0;h<g;h++)n[h]^=1549556828,j[h]^=909522486;p.sigBytes=b.sigBytes=k;this.reset()},reset:function(){var e=this._hasher;e.reset();e.update(this._iKey)},update:function(e){this._hasher.update(e);return this},finalize:function(e){var d=
this._hasher;e=d.finalize(e);d.reset();return d.finalize(this._oKey.clone().concat(e))}})})();
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}},
u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a,
d)).finalize(c)}}});var t=f.algo={};return f}(Math);
(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]=
c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math);
(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g=
this._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})();
//oauth-1.0a
if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') {
module.exports = OAuth;
}
/**
* Constructor
* #param {Object} opts consumer key and secret
*/
function OAuth(opts) {
if(!(this instanceof OAuth)) {
return new OAuth(opts);
}
if(!opts) {
opts = {};
}
if(!opts.consumer) {
throw new Error('consumer option is required');
}
this.consumer = opts.consumer;
this.nonce_length = opts.nonce_length || 32;
this.version = opts.version || '1.0';
this.parameter_seperator = opts.parameter_seperator || ', ';
this.realm = opts.realm;
if(typeof opts.last_ampersand === 'undefined') {
this.last_ampersand = true;
} else {
this.last_ampersand = opts.last_ampersand;
}
// default signature_method is 'PLAINTEXT'
this.signature_method = opts.signature_method || 'PLAINTEXT';
if(this.signature_method == 'PLAINTEXT' && !opts.hash_function) {
opts.hash_function = function(base_string, key) {
return key;
}
}
if(!opts.hash_function) {
throw new Error('hash_function option is required');
}
this.hash_function = opts.hash_function;
this.body_hash_function = opts.body_hash_function || this.hash_function;
}
/**
* OAuth request authorize
* #param {Object} request data
* {
* method,
* url,
* data
* }
* #param {Object} key and secret token
* #return {Object} OAuth Authorized data
*/
OAuth.prototype.authorize = function(request, token) {
var oauth_data = {
oauth_consumer_key: this.consumer.key,
oauth_nonce: this.getNonce(),
oauth_signature_method: this.signature_method,
oauth_timestamp: this.getTimeStamp(),
oauth_version: this.version
};
if(!token) {
token = {};
}
if(token.key !== undefined) {
oauth_data.oauth_token = token.key;
}
if(!request.data) {
request.data = {};
}
if(request.includeBodyHash) {
oauth_data.oauth_body_hash = this.getBodyHash(request, token.secret)
}
oauth_data.oauth_signature = this.getSignature(request, token.secret, oauth_data);
return oauth_data;
};
/**
* Create a OAuth Signature
* #param {Object} request data
* #param {Object} token_secret key and secret token
* #param {Object} oauth_data OAuth data
* #return {String} Signature
*/
OAuth.prototype.getSignature = function(request, token_secret, oauth_data) {
return this.hash_function(this.getBaseString(request, oauth_data), this.getSigningKey(token_secret));
};
/**
* Create a OAuth Body Hash
* #param {Object} request data
*/
OAuth.prototype.getBodyHash = function(request, token_secret) {
var body = typeof request.data === 'string' ? request.data : JSON.stringify(request.data)
if (!this.body_hash_function) {
throw new Error('body_hash_function option is required');
}
return this.body_hash_function(body, this.getSigningKey(token_secret))
};
/**
* Base String = Method + Base Url + ParameterString
* #param {Object} request data
* #param {Object} OAuth data
* #return {String} Base String
*/
OAuth.prototype.getBaseString = function(request, oauth_data) {
return request.method.toUpperCase() + '&' + this.percentEncode(this.getBaseUrl(request.url)) + '&' + this.percentEncode(this.getParameterString(request, oauth_data));
};
/**
* Get data from url
* -> merge with oauth data
* -> percent encode key & value
* -> sort
*
* #param {Object} request data
* #param {Object} OAuth data
* #return {Object} Parameter string data
*/
OAuth.prototype.getParameterString = function(request, oauth_data) {
var base_string_data;
if (oauth_data.oauth_body_hash) {
base_string_data = this.sortObject(this.percentEncodeData(this.mergeObject(oauth_data, this.deParamUrl(request.url))));
} else {
base_string_data = this.sortObject(this.percentEncodeData(this.mergeObject(oauth_data, this.mergeObject(request.data, this.deParamUrl(request.url)))));
}
var data_str = '';
//base_string_data to string
for(var i = 0; i < base_string_data.length; i++) {
var key = base_string_data[i].key;
var value = base_string_data[i].value;
// check if the value is an array
// this means that this key has multiple values
if (value && Array.isArray(value)){
// sort the array first
value.sort();
var valString = "";
// serialize all values for this key: e.g. formkey=formvalue1&formkey=formvalue2
value.forEach((function(item, i){
valString += key + '=' + item;
if (i < value.length){
valString += "&";
}
}).bind(this));
data_str += valString;
} else {
data_str += key + '=' + value + '&';
}
}
//remove the last character
data_str = data_str.substr(0, data_str.length - 1);
return data_str;
};
/**
* Create a Signing Key
* #param {String} token_secret Secret Token
* #return {String} Signing Key
*/
OAuth.prototype.getSigningKey = function(token_secret) {
token_secret = token_secret || '';
if(!this.last_ampersand && !token_secret) {
return this.percentEncode(this.consumer.secret);
}
return this.percentEncode(this.consumer.secret) + '&' + this.percentEncode(token_secret);
};
/**
* Get base url
* #param {String} url
* #return {String}
*/
OAuth.prototype.getBaseUrl = function(url) {
return url.split('?')[0];
};
/**
* Get data from String
* #param {String} string
* #return {Object}
*/
OAuth.prototype.deParam = function(string) {
var arr = string.split('&');
var data = {};
for(var i = 0; i < arr.length; i++) {
var item = arr[i].split('=');
// '' value
item[1] = item[1] || '';
// check if the key already exists
// this can occur if the QS part of the url contains duplicate keys like this: ?formkey=formvalue1&formkey=formvalue2
if (data[item[0]]){
// the key exists already
if (!Array.isArray(data[item[0]])) {
// replace the value with an array containing the already present value
data[item[0]] = [data[item[0]]];
}
// and add the new found value to it
data[item[0]].push(decodeURIComponent(item[1]));
} else {
// it doesn't exist, just put the found value in the data object
data[item[0]] = decodeURIComponent(item[1]);
}
}
return data;
};
/**
* Get data from url
* #param {String} url
* #return {Object}
*/
OAuth.prototype.deParamUrl = function(url) {
var tmp = url.split('?');
if (tmp.length === 1)
return {};
return this.deParam(tmp[1]);
};
/**
* Percent Encode
* #param {String} str
* #return {String} percent encoded string
*/
OAuth.prototype.percentEncode = function(str) {
return encodeURIComponent(str)
.replace(/\!/g, "%21")
.replace(/\*/g, "%2A")
.replace(/\'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
};
/**
* Percent Encode Object
* #param {Object} data
* #return {Object} percent encoded data
*/
OAuth.prototype.percentEncodeData = function(data) {
var result = {};
for(var key in data) {
var value = data[key];
// check if the value is an array
if (value && Array.isArray(value)){
var newValue = [];
// percentEncode every value
value.forEach((function(val){
newValue.push(this.percentEncode(val));
}).bind(this));
value = newValue;
} else {
value = this.percentEncode(value);
}
result[this.percentEncode(key)] = value;
}
return result;
};
/**
* Get OAuth data as Header
* #param {Object} oauth_data
* #return {String} Header data key - value
*/
OAuth.prototype.toHeader = function(oauth_data) {
var sorted = this.sortObject(oauth_data);
var header_value = 'OAuth ';
if (this.realm) {
header_value += 'realm="' + this.realm + '"' + this.parameter_seperator;
}
for(var i = 0; i < sorted.length; i++) {
if (sorted[i].key.indexOf('oauth_') !== 0)
continue;
header_value += this.percentEncode(sorted[i].key) + '="' + this.percentEncode(sorted[i].value) + '"' + this.parameter_seperator;
}
return {
Authorization: header_value.substr(0, header_value.length - this.parameter_seperator.length) //cut the last chars
};
};
/**
* Create a random word characters string with input length
* #return {String} a random word characters string
*/
OAuth.prototype.getNonce = function() {
var word_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var result = '';
for(var i = 0; i < this.nonce_length; i++) {
result += word_characters[parseInt(Math.random() * word_characters.length, 10)];
}
return result;
};
/**
* Get Current Unix TimeStamp
* #return {Int} current unix timestamp
*/
OAuth.prototype.getTimeStamp = function() {
return parseInt(new Date().getTime()/1000, 10);
};
////////////////////// HELPER FUNCTIONS //////////////////////
/**
* Merge object
* #param {Object} obj1
* #param {Object} obj2
* #return {Object}
*/
OAuth.prototype.mergeObject = function(obj1, obj2) {
obj1 = obj1 || {};
obj2 = obj2 || {};
var merged_obj = obj1;
for(var key in obj2) {
merged_obj[key] = obj2[key];
}
return merged_obj;
};
/**
* Sort object by key
* #param {Object} data
* #return {Array} sorted array
*/
OAuth.prototype.sortObject = function(data) {
var keys = Object.keys(data);
var result = [];
keys.sort();
for(var i = 0; i < keys.length; i++) {
var key = keys[i];
result.push({
key: key,
value: data[key],
});
}
return result;
};
//NETSUITE'S RESTLET
function callRESTlet(request, response) {
var remoteAccountID = 'ACCOUNT ID HERE';
var restletUrl = 'https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1';
//user token
var token = {
key: 'ACCESS KEY HERE',
secret: 'ACCESS SECRET HERE'
};
//app credentials
var oauth = OAuth({
consumer: {
key: 'INTEGRATION KEY HERE',
secret: 'INTEGRATION SECRET HERE'
},
signature_method: 'HMAC-SHA1',
hash_function: function(base_string, key)
{
return CryptoJS.HmacSHA1(base_string, key).toString(CryptoJS.enc.Base64);
}
});
var request_data = {
url: restletUrl,
method: 'GET',
data: {}
};
var oauth_data = {
oauth_consumer_key: oauth.consumer.key,
oauth_nonce: oauth.getNonce(),
oauth_signature_method: oauth.signature_method,
oauth_timestamp: oauth.getTimeStamp(),
oauth_version: '1.0',
oauth_token: token.key,
realm: remoteAccountID
};
var headerWithRealm = oauth.toHeader(oauth.authorize(request_data, token));
headerWithRealm.Authorization += ',realm="' + remoteAccountID + '"';
var restResponse = nlapiRequestURL(restletUrl, null, headerWithRealm, null, "GET");
var html = 'Calling: ' +
restletUrl +
'<br><br>' +
'Generated OAuth header:<br>' +
headerWithRealm.Authorization +
'<br><br>' +
'Response:<br>' +
restResponse.getBody()
response.write(html);
}
EDIT: Just published an npm module which should make things easier: https://www.npmjs.com/package/nsrestlet
Was able to get some code working after hunting through GitHub Code commits. Still, bknights response is really good.
Here's what I got working.
Assuming you have Node.js and npm installed, run:
npm install request
npm install oauth-1.0a#1.0.1
It's really important that it's version 1.0.1.
Once you have that, this code should work:
/*
================= REQUIRED USER ACCOUNT INFORMATION ==============================================
*/
var accountID = 'PUT ACCOUNT ID HERE';
var token = {
public: 'PUT TOKEN KEY HERE',
secret: 'PUB TOKEN SECRET HERE'
};
var consumer = {
public: 'PUT CONSUMER KEY HERE',
secret: 'PUT CONSUMER SECRET HERE'
};
//use the full restlet URL, not the rest.netsuite.com URL
//for example, https://YOURACCOUNTNUMBER.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=SCRIPTNUMBER&deploy=DEPLOYNUMBER
var restlet_url = 'PUT YOUR RESTLET URL HERE';
/*
=========================================================================================================
*/
//REQUIRED NPM MODULES
const request = require('request');
const OAuth = require('oauth-1.0a'); //version 1.0.1, don't do version 1.1.0
//SET UP THE OAUTH OBJECT
var oauth = OAuth({
consumer: consumer,
signature_method: 'HMAC-SHA256' //you can also use HMAC-SHA1 but HMAC-SHA256 is more secure (supposedly)
});
//SET UP THE REQUEST OBJECT
var request_data = {
url: restlet_url,
method: 'POST',
};
//GET THE AUTHORIZATION AND STICK IT IN THE HEADER, ALONG WITH THE REALM AND CONTENT-TYPE
var authorization = oauth.authorize(request_data, token);
var header = oauth.toHeader(authorization);
header.Authorization += ', realm="' + accountID + '"';
header['content-type'] = 'application/json';
//MAKE THE REQUEST
request({
url: request_data.url,
method: request_data.method,
headers: header,
json: {
message: "test123" //this is your payload
}
}, function(error, response, body) {
if(error)
{
console.log(error);
}
else
{
console.log(body);
}
});
If anybody has any problems with this code, leave a response and I'll do my best to help.
Netsuite's node samples use oauth-1.0a
https://netsuite.custhelp.com/app/answers/detail/a_id/42171
and their sample from https://netsuite.custhelp.com/app/answers/detail/a_id/42172/
is what I've had in production for a couple of years and works well.
var Promise = require('bluebird');
var request = require('request');
var crypto = require('crypto');
var OAuth = require('oauth-1.0a');
var Agent = require('https').Agent;
//var debug = require('debug')('kotn-ns');
function promiseTry(pSrc, maxTries, minDelay, maxDelay, canRetry){ //NS prone to spurious failures due to overloading
return new Promise(function(resolve, reject){
minDelay = minDelay || 0;
var delaySize = maxDelay - minDelay;
var t = function(){ return Math.floor(Math.random()* delaySize)+ minDelay;};
var firstReason = null;
function doRetry(triesLeft){
pSrc().then(function(data){
resolve(data);
}).catch(function(reason){
if(!firstReason) firstReason = reason;
console.error('in retry error with '+reason.toString());
if(triesLeft && canRetry(reason)) setTimeout(function(){ doRetry(triesLeft-1);}, t());
else reject(firstReason);
});
}
doRetry(maxTries -1);
});
}
function hasReason(msg, reasons){
for(var i = 0; i< reasons.length;i++){
if(msg.indexOf(reasons[i]) != -1) return true;
}
return false;
}
var agentPool = {};
function getAgent(accountId, tokenId){
var agentKey = accountId+'::'+ tokenId;
var agent = agentPool[agentKey];
if(!agent){
console.log('new agent for '+agentKey)
agent = new Agent({
keepAlive:false,
maxSockets:5
});
agentPool[agentKey] = agent;
}
return agent;
}
/**
* [RESTHandler description]
* #param {options} options {accountId, consumerKey,consumerSecret,tokenId,tokenSecret}
*/
function RESTHandler(options) {
var config = Object.assign({
maxTries:3,
minRetryDelay: 800,
maxRetryDelay:30000,
canRetry: function(reason){
var reasonText = reason.message || JSON.stringify(reason);
if(hasReason(reasonText, ['ECONNRESET', 'ESOCKETTIMEDOUT','ETIMEDOUT', 'SSS_REQUEST_LIMIT_EXCEEDED'])) {
console.error('retrying because: '+reasonText);
return true;
}
console.error('no retry with: '+reasonText);
return false;
}
}, options);
var oauth = OAuth({
consumer: {
key: config.consumerKey,
secret: config.consumerSecret
},
signature_method: 'HMAC-SHA1',
parameter_seperator: ',',
hash_function: function(base_string, key) {
return crypto.createHmac('sha1', key).update(base_string).digest('base64');
}
});
var token = {
key: config.tokenId,
secret: config.tokenSecret
};
function makeRequest(url, method, payload) {
var requestData = {
url: url,
method: method
};
if(payload){
requestData.body = payload;
}
var headers = oauth.toHeader(oauth.authorize(requestData, token));
headers.Authorization += ',realm="' + config.accountId + '"';
headers.authorization = headers.Authorization;
delete headers.Authorization;
headers['content-type'] = 'application/json';
headers['accept'] = 'application/json';
//console.log(JSON.stringify(headers, null, ' '));
requestData.headers = headers;
Object.assign(requestData, {
pool:getAgent(config.accountId, config.tokenId),
timeout : 30000,
strictSSL : true
});
// requestData.json = true;
// return new Promise(function(resolve){
// resolve({'headers' : 'done'});
// });
var processRequest = function(){
return new Promise(function(resolve, reject) {
request(requestData, function(error, response, body) {
if(error){
console.error('error calling: '+ requestData.url);
console.error(error);
reject((error instanceof Error) ? error : new Error(JSON.stringify(error)));
return;
}
if(!body || !(/"success"/).test(body)) {
console.log(method +' '+ response.statusCode +' '+ url +'\n\t'+body);
reject(new Error(body || 'unexpected error'));
return;
}
try{
resolve(JSON.parse(body));
}catch(e){
console.trace(e);
reject(e);
}
});
});
};
return promiseTry(processRequest, config.maxTries, config.maxRetryDelay, config.minRetryDelay, config.canRetry);
}
return{
get: function(url){
return makeRequest(url, 'GET');
},
put: function(url, data){
return makeRequest(url, 'PUT', data);
},
post: function(url, data){
return makeRequest(url, 'POST', data);
},
destroy : function(){
//nsAgent.destroy();
}
};
}
module.exports = RESTHandler;
More up-to-date as of 2022-12-12
replaced request with its successor needle and removed the now unnecessary bluebird
const needle = require('needle');
const crypto = require('crypto');
const OAuth = require('oauth-1.0a');
const Agent = require('https').Agent;
const debug = require('debug')('kotn-nso');
function promiseTry(pSrc, maxTries, minDelay, maxDelay, canRetry){ //NS prone to spurious failures due to overloading
return new Promise((resolve, reject)=>{
minDelay = minDelay || 0;
const delaySize = maxDelay - minDelay;
const t = function(){ return Math.floor(Math.random()* delaySize)+ minDelay;};
let firstReason = null;
function doRetry(triesLeft){
pSrc().then((data)=>{
resolve(data);
}).catch((reason)=>{
if(!firstReason) firstReason = reason;
console.error('in retry error with '+ triesLeft +' for '+reason.toString());
if(triesLeft && canRetry(reason)){
setTimeout(()=>{ doRetry(triesLeft-1);}, t());
}
else reject(firstReason);
});
}
doRetry(maxTries -1);
});
}
function hasReason(msg, reasons){
for(var i = 0; i< reasons.length;i++){
if(msg.indexOf(reasons[i]) != -1) return true;
}
return false;
}
var agentPool = {};
function getAgent(accountId, maxSockets){
var agentKey = accountId+'::'; // + tokenId;
var agent = agentPool[agentKey];
if(!agent){
console.log('new agent for '+agentKey);
agent = new Agent({
keepAlive:false,
maxSockets:maxSockets || 2 // one in reserve for slow closers
});
agentPool[agentKey] = agent;
}
return agent;
}
/**
* [RESTHandler description]
* #param {options} options {accountId, consumerKey,consumerSecret,tokenId,tokenSecret,maxSockets}
*/
function RESTHandler(options) {
const config = Object.assign({
maxTries:3,
minRetryDelay: 400,
maxRetryDelay:30000,
canRetry: function(reason){
var reasonText = reason.message || JSON.stringify(reason);
if(hasReason(reasonText, ['ECONNRESET', 'ESOCKETTIMEDOUT','ETIMEDOUT', 'SSS_REQUEST_LIMIT_EXCEEDED'])) {
console.error('retrying because: '+reasonText);
return true;
}
console.error('no retry with: '+reasonText);
return false;
}
}, options);
const oauth = OAuth({
consumer: {
key: config.consumerKey,
secret: config.consumerSecret
},
signature_method: 'HMAC-SHA256',
parameter_seperator: ',',
hash_function: function(base_string, key) {
return crypto.createHmac('sha256', key).update(base_string).digest('base64');
}
});
const token = {
key: config.tokenId,
secret: config.tokenSecret
};
function makeRequest(url, method, payload) {
debug(method +' '+ JSON.stringify(url));
var requestData = {
url: url,
method: method
};
if(payload){
requestData.body = payload;
}
var headers = oauth.toHeader(oauth.authorize(requestData, token));
headers.Authorization += ',realm="' + config.accountId + '"';
headers.authorization = headers.Authorization;
delete headers.Authorization;
headers['content-type'] = 'application/json';
headers['accept'] = 'application/json';
//console.log(JSON.stringify(headers, null, ' '));
const options = {
headers:headers,
agent:getAgent(config.accountId, config.maxSockets),
timeout : 30000,
strictSSL : true,
time:false
};
var processRequest = function(){
var headers = null;
return needle(method.toLowerCase(), url, payload, options).then(resp=>{
debug(resp.statusCode +' ' + JSON.stringify(resp.headers, null, ' '));
headers = resp.headers;
if(resp.statusCode != 200 && resp.statusCode != 201){
throw new Error(resp.statusCode +': '+ (resp.body || 'unexpected error'));
}
if(!resp.body) throw new Error('Unexpected Response');
return resp.body;
}).catch(error=>{
if(headers){
debug('Error returned with ' + JSON.stringify(headers));
}
const msg = error instanceof Error ? error.message : JSON.stringify(error);
console.error(method +' error on '+ url + ', '+ msg);
throw (error instanceof Error) ? error : new Error(msg);
});
};
return promiseTry(processRequest, config.maxTries, config.maxRetryDelay, config.minRetryDelay, config.canRetry);
}
return{
get: function(url){
return makeRequest(url, 'GET');
},
put: function(url, data){
return makeRequest(url, 'PUT', data);
},
post: function(url, data){
return makeRequest(url, 'POST', data);
},
destroy : function(){
//nsAgent.destroy();
}
};
}
module.exports = RESTHandler;

In NetSuite with SuiteScript 2.0 unable to send a file with HTTP POST request with a content-type multipart/form-data

I am not able to send my "multipart/form-data' file to this API. If I use POSTMAN it's working but with the https post method it seems that netsuite doesn't recognize the "form-data" content-type. Somebody knows how to send a form-data with SuiteScript 2 ? Here is a part of my code:
var fileObj = file.create({
name: invoiceNumber + '_ubl.xml',
fileType: file.Type.XMLDOC,
contents: einvoicecontentwithpdf,
folder : 120,
isOnline : false
});
var headers = {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxx',
'Content-Type': 'multipart/form-data'
};
var response = https.post({
url: 'https://community-api-uat-1-2.nxt.uat.unifiedpost.com/api/universal_connector/v1/uc/ar',
body: {
'file': fileObj
},
headers: headers
});
You have to build the whole body yourself.
This works as much as I've tested so far. Code may look a little non-idomatic because it's compiled from Typescript.
Note that to include non-text files this would have to be adapted to base64 encode the file bodies.
/**
* multiPartUpload.js
* #NApiVersion 2.x
*/
define(["require", "exports", "N/https", "N/file", "N/log"], function (require, exports, http, file, log) {
Object.defineProperty(exports, "__esModule", { value: true });
var types = {};
types[file.Type.AUTOCAD] = 'application/x-autocad';
types[file.Type.BMPIMAGE] = 'image/x-xbitmap';
types[file.Type.CSV] = 'text/csv';
types[file.Type.EXCEL] = 'application/vnd.ms-excel';
types[file.Type.FLASH] = 'application/x-shockwave-flash';
types[file.Type.GIFIMAGE] = 'image/gif';
types[file.Type.GZIP] = 'application/?x-?gzip-?compressed';
types[file.Type.HTMLDOC] = 'text/html';
types[file.Type.ICON] = 'image/ico';
types[file.Type.JAVASCRIPT] = 'text/javascript';
types[file.Type.JPGIMAGE] = 'image/jpeg';
types[file.Type.JSON] = 'application/json';
types[file.Type.MESSAGERFC] = 'message/rfc822';
types[file.Type.MP3] = 'audio/mpeg';
types[file.Type.MPEGMOVIE] = 'video/mpeg';
types[file.Type.MSPROJECT] = 'application/vnd.ms-project';
types[file.Type.PDF] = 'application/pdf';
types[file.Type.PJPGIMAGE] = 'image/pjpeg';
types[file.Type.PLAINTEXT] = 'text/plain';
types[file.Type.PNGIMAGE] = 'image/x-png';
types[file.Type.POSTSCRIPT] = 'application/postscript';
types[file.Type.POWERPOINT] = 'application/?vnd.?ms-?powerpoint';
types[file.Type.QUICKTIME] = 'video/quicktime';
types[file.Type.RTF] = 'application/rtf';
types[file.Type.SMS] = 'application/sms';
types[file.Type.STYLESHEET] = 'text/css';
types[file.Type.TIFFIMAGE] = 'image/tiff';
types[file.Type.VISIO] = 'application/vnd.visio';
types[file.Type.WORD] = 'application/msword';
types[file.Type.XMLDOC] = 'text/xml';
types[file.Type.ZIP] = 'application/zip';
function getContentType(f) {
var mime = types[f.fileType];
var charset = f.encoding;
var ct = 'Content-Type: ' + mime + (charset ? ';charset=' + charset : '');
log.debug({ title: 'content for ' + f.name, details: ct });
return ct;
}
function isFile(o) {
return (typeof o == 'object' && typeof o.fileType != 'undefined');
}
/**
* Creates a multipart upload
* #param {string} url to post to
* #param {object} headers key/value of headers; include Auth headers if needed
* #param {array} parts array of {name:string, value:file|string}
*/
function uploadParts(url, headers, parts) {
var boundary = 'someuniqueboundaryasciistring';
headers['content-type'] = 'multipart/form-data; boundary=' + boundary;
// Body
var body = [];
parts.forEach(function (p, idx) {
var partIsFile = isFile(p.value);
body.push('--' + boundary);
body.push('Content-Disposition: form-data; name="' + p.name + '"' + (partIsFile ? ('; filename="' + p.value.name + '"') : ''));
if (partIsFile) {
var ct = getContentType(p.value);
body.push(getContentType(p.value));
if(partIsBinary(ct)){
body.push('Content-Transfer-Encoding: base64');
}
}
body.push('');
body.push(partIsFile ? p.value.getContents() : p.value);
if (idx == parts.length - 1) {
body.push('--' + boundary + '--');
body.push('');
}
});
// Submit Request
try {
var response = http.post({
url: url,
headers: headers,
body: body.join('\r\n')
});
return response;
}
catch (e) {
log.error({ title: 'Failed to submit file', details: (e.message || e.toString()) + (e.getStackTrace ? (' \n \n' + e.getStackTrace().join(' \n')) : '') });
}
}
exports.uploadParts = uploadParts;
;
});
a simple test:
/**
*#NApiVersion 2.x
*#NScriptType Suitelet
*/
define(["require", "exports", "N/file", "./multiPartUpload"], function (require, exports, file, multiPartUpload_1) {
Object.defineProperty(exports, "__esModule", { value: true });
function onRequest(context) {
var files = [
{ name: 'f1', value: file.load({ id: 1056 }) }, // file cabinet ids; you can use dynamic files
{ name:'t1', value:'test'},
{ name: 'f2', value: file.load({ id: 7479 }) }
];
var resp = multiPartUpload_1.uploadParts('https://your-test-server/', {}, files);
context.response.write({ output: resp.body });
}
exports.onRequest = onRequest;
});

Cant stream video (PFFile) from parse server

I am having trouble streaming video with my iOS app from URL of a PFFile uploaded in my database. I used Heroku and AWS and I still have the same issue. It used to work fine when the files were hosted in the old parse server.
the PFFile url works fine when I open it in a chrome web browser but not in safari nor in the iOS app.
the following is the link of the video:
http://shuuapp.herokuapp.com/parse/files/wnQeou0L4klDelSEtMOX6SxXRVKu1f3sKl6vg349/24092609eadcc049f711aafbd59c1a18_movie.mp4
Its exactly the same issue as the issue mentioned in the link below:
iOS - Can't stream video from Parse Backend
parse-server doesn't seem to be supporting streaming in Safari/iOS and the solution is the enable it using express & GridStore as follows,
parse-server-example\node_modules\parse-server\lib\Routers\FilesRouter
{
key: 'getHandler',
value: function getHandler(req, res, content) {
var config = new _Config2.default(req.params.appId);
var filesController = config.filesController;
var filename = req.params.filename;
var video = '.mp4'
var lastFourCharacters = video.substr(video.length - 4);
if (lastFourCharacters == '.mp4') {
filesController.handleVideoStream(req, res, filename).then(function (data) {
}).catch(function (err) {
console.log('404FilesRouter');
res.status(404);
res.set('Content-Type', 'text/plain');
res.end('File not found.');
});
}else{
filesController.getFileData(config, filename).then(function (data) {
res.status(200);
res.end(data);
}).catch(function (err) {
res.status(404);
res.set('Content-Type', 'text/plain');
res.end('File not found.');
});
}
}
} , ...
parse-server-example\node_modules\parse-server\lib\Controllers\FilesController
_createClass(FilesController, [{
key: 'getFileData',
value: function getFileData(config, filename) {
return this.adapter.getFileData(filename);
}
},{
key: 'handleVideoStream',
value: function handleVideoStream(req, res, filename) {
return this.adapter.handleVideoStream(req, res, filename);
}
}, ...
parse-server-example\node_modules\parse-server\lib\Adapters\Files\GridStoreAdapter
... , {
key: 'handleVideoStream',
value: function handleVideoStream(req, res, filename) {
return this._connect().then(function (database) {
return _mongodb.GridStore.exist(database, filename).then(function () {
var gridStore = new _mongodb.GridStore(database, filename, 'r');
gridStore.open(function(err, GridFile) {
if(!GridFile) {
res.send(404,'Not Found');
return;
}
console.log('filename');
StreamGridFile(GridFile, req, res);
});
});
})
}
}, ...
Bottom of GridStore Adapter
function StreamGridFile(GridFile, req, res) {
var buffer_size = 1024 * 1024;//1024Kb
if (req.get('Range') != null) { //was: if(req.headers['range'])
// Range request, partialle stream the file
console.log('Range Request');
var parts = req.get('Range').replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = partialstart ? parseInt(partialstart, 10) : 0;
var end = partialend ? parseInt(partialend, 10) : GridFile.length - 1;
var chunksize = (end - start) + 1;
if(chunksize == 1){
start = 0;
partialend = false;
}
if(!partialend){
if(((GridFile.length-1) - start) < (buffer_size) ){
end = GridFile.length - 1;
}else{
end = start + (buffer_size);
}
chunksize = (end - start) + 1;
}
if(start == 0 && end == 2){
chunksize = 1;
}
res.writeHead(206, {
'Cache-Control': 'no-cache',
'Content-Range': 'bytes ' + start + '-' + end + '/' + GridFile.length,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
});
GridFile.seek(start, function () {
// get GridFile stream
var stream = GridFile.stream(true);
var ended = false;
var bufferIdx = 0;
var bufferAvail = 0;
var range = (end - start) + 1;
var totalbyteswanted = (end - start) + 1;
var totalbyteswritten = 0;
// write to response
stream.on('data', function (buff) {
bufferAvail += buff.length;
//Ok check if we have enough to cover our range
if(bufferAvail < range) {
//Not enough bytes to satisfy our full range
if(bufferAvail > 0)
{
//Write full buffer
res.write(buff);
totalbyteswritten += buff.length;
range -= buff.length;
bufferIdx += buff.length;
bufferAvail -= buff.length;
}
}
else{
//Enough bytes to satisfy our full range!
if(bufferAvail > 0) {
var buffer = buff.slice(0,range);
res.write(buffer);
totalbyteswritten += buffer.length;
bufferIdx += range;
bufferAvail -= range;
}
}
if(totalbyteswritten >= totalbyteswanted) {
// totalbytes = 0;
GridFile.close();
res.end();
this.destroy();
}
});
});
}else{
// res.end(GridFile);
// stream back whole file
res.header('Cache-Control', 'no-cache');
res.header('Connection', 'keep-alive');
res.header("Accept-Ranges", "bytes");
res.header('Content-Type', 'video/mp4');
res.header('Content-Length', GridFile.length);
var stream = GridFile.stream(true).pipe(res);
}
};
P.S
The original answer is given by #Bragegs here - https://github.com/ParsePlatform/parse-server/issues/1440#issuecomment-212815625 .

Resources