How to encode zip files as part of multipart/form-data - node.js

tldr I'm having encoding issues when sending zip files as part of a multipart/form-data request body. Help please :/
I'm working on a bot that should be able to upload zip files to Slack (via their file api), but I'm running into some issues that I believe are related to encoding.
So, I'm creating my request body as follows:
var form_string = "\n--abcdefghijklmnop\nContent-Disposition: form-data; filename=\"" + filename + "\"; name=\"file\";\nContent-Type:application/octet-stream;\nContent-Transfer-Encoding:base64;\n\n" + data;
form_string += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"token\";\n\n" + token;
form_string += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"filetype\";\n\n" + filetype;
form_string += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"filename\";\n\n" + filename;
form_string += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"channels\";\n\n" + channel;
form_string += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"title\";\n\n" + title;
form_string += "\n--abcdefghijklmnop--";
var form = Buffer.from(form_string, "utf8");
var headers = {
"Content-Type": "multipart/form-data; boundary=abcdefghijklmnop",
"Content-Length": form.length,
"Authorization": "Bearer ....."
};
var options = {
"headers": headers,
"body": form
};
// using the sync-request node module.
var res = request("POST", url, options);
var res = request("POST", url, options);
(I've tried application/zip and application/x-zip-compressed as well. I've also tried both binary and base64 content transfer encodings.)
(And in case you're wondering, I need to make synchronous http requests...)
I created a really small zip file as a test. The base64 encoding of it is below:
UEsDBAoAAAAAAAqR+UoAAAAAAAAAAAAAAAAIABwAdGlueXppcC9VVAkAA1PBd1mDwXdZdXgLAAEE9QEAAAQUAAAAUEsDBAoAAAAAAAuR+Up6em/tAwAAAAMAAAAQABwAdGlueXppcC90aW55LnR4dFVUCQADVsF3WVzBd1l1eAsAAQT1AQAABBQAAABoaQpQSwECHgMKAAAAAAAKkflKAAAAAAAAAAAAAAAACAAYAAAAAAAAABAA7UEAAAAAdGlueXppcC9VVAUAA1PBd1l1eAsAAQT1AQAABBQAAABQSwECHgMKAAAAAAALkflKenpv7QMAAAADAAAAEAAYAAAAAAABAAAApIFCAAAAdGlueXppcC90aW55LnR4dFVUBQADVsF3WXV4CwABBPUBAAAEFAAAAFBLBQYAAAAAAgACAKQAAACPAAAAAAA=
What I'm getting from Slack seems to be similar to the original... maybe...
UEsDBAoAAAAAAArCkcO5SgAAAAAAAAAAAAAAAAgAHAB0aW55emlwL1VUCQADU8OBd1nCg8OBd1l1eAsAAQTDtQEAAAQUAAAAUEsDBAoAAAAAAAvCkcO5Snp6b8OtAwAAAAMAAAAQABwAdGlueXppcC90aW55LnR4dFVUCQADVsOBd1lcw4F3WXV4CwABBMO1AQAABBQAAABoaQpQSwECHgMKAAAAAAAKwpHDuUoAAAAAAAAAAAAAAAAIABgAAAAAAAAAEADDrUEAAAAAdGlueXppcC9VVAUAA1PDgXdZdXgLAAEEw7UBAAAEFAAAAFBLAQIeAwoAAAAAAAvCkcO5Snp6b8OtAwAAAAMAAAAQABgAAAAAAAEAAADCpMKBQgAAAHRpbnl6aXAvdGlueS50eHRVVAUAA1bDgXdZdXgLAAEEw7UBAAAEFAAAAFBLBQYAAAAAAgACAMKkAAAAwo8AAAAAAA==
Could someone explain what encoding is going on here and how I can correctly upload a file to Slack? Thanks!

How about following sample scripts? There are 2 patterns for this situation.
Sample script 1 :
For this, I modified the method you are trying. You can upload the zip file by converting to the byte array as follows. At first, it builds form-data. It adds the zip file converted to byte array and boundary using Buffer.concat(). This is used as body in request.
var fs = require('fs');
var request = require('request');
var upfile = 'sample.zip';
fs.readFile(upfile, function(err, content){
if(err){
console.error(err);
}
var token = '### access token ###';
var filetype = 'zip';
var filename = 'samplefilename';
var channel = 'sample';
var title = 'sampletitle';
var formString = "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"token\";\n\n" + token;
formString += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"filetype\";\n\n" + filetype;
formString += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"filename\";\n\n" + filename;
formString += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"channels\";\n\n" + channel;
formString += "\n--abcdefghijklmnop\nContent-Disposition: form-data; name=\"title\";\n\n" + title;
formString += "\n--abcdefghijklmnop\nContent-Disposition: form-data; filename=\"" + upfile + "\"; name=\"file\";\nContent-Type:application/octet-stream;\n\n";
var options = {
method: 'post',
url: 'https://slack.com/api/files.upload',
headers: {"Content-Type": "multipart/form-data; boundary=abcdefghijklmnop"},
body: Buffer.concat([
Buffer.from(formString, "utf8"),
new Buffer(content, 'binary'),
Buffer.from("\n--abcdefghijklmnop\n", "utf8"),
]),
};
request(options, function(error, response, body) {
console.log(body);
});
});
Sample script 2 :
This is a simpler way than sample 1. You can use fs.createReadStream() as a file for uploading to Slack.
var fs = require('fs');
var request = require('request');
request.post({
url: 'https://slack.com/api/files.upload',
formData: {
file: fs.createReadStream('sample.zip'),
token: '### access token ###',
filetype: 'zip',
filename: 'samplefilename',
channels: 'sample',
title: 'sampletitle',
},
}, function(error, response, body) {
console.log(body);
});
Result :
Both sample 1 and sample 2 can be uploaded zip file to Slack as follows. For both, even if filetype is not defined, the uploaded file is used automatically as a zip file.
If I misunderstand your question, I'm sorry.

Related

Google Drive REST API - Corrupted ZIP file after upload

I'm using the Google Drive REST API to upload a ZIP file but all my ZIP files become corrupted after the upload. When I download the file and then try to unzip it on my computer, on MacOS it says "Unable to expand 'FILE_NAME.zip' into FOLDER. Error 79 - Inappropriate file type or format.". I made sure it wasn't just my computer by having another person on a different computer try to unzip it and they had the same problem. I also confirmed that the ZIP file wasn't becoming corrupted before I uploaded it to Google Drive.
Below is a simplified version of my code.
const async = require('async');
const requestModule = require('request');
const fs = require('fs');
var api = {};
var tasks = {
// first, get the zip file contents
'getFile': function(cb) {
fs.readFile('my_file.zip', {'encoding':'UTF-8'}, function(err, data) {
if (err) {
console.error(err);
return cb();
}
api.file_data = data;
cb();
});
},
// second, upload the file contents to google drive via their API
'uploadFile': function(cb) {
var metadata = {
'mimeType': 'application/zip',
'name': 'my_file.zip'
};
var request = {
'url': 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true',
'method': 'POST',
'headers': {
'Authorization': 'Bearer ' + GOOGLE_ACCESS_TOKEN,
'Content-Type': 'multipart/related; boundary="SECTION"'
},
'body': '--SECTION\r\n' +
'Content-Type: application/json; charset=UTF-8\r\n' +
'\r\n' +
JSON.stringify(metadata) + '\r\n' +
'\r\n' +
'--SECTION\r\n' +
'Content-Type: application/zip\r\n' +
'Content-Transfer-Encoding: base64\r\n' +
'\r\n' +
new Buffer.from(api.file_data).toString('base64') + '\r\n' +
'\r\n' +
'--SECTION--'
};
requestModule(request, function(err, res, body) {
if (err) {
console.error(err);
return cb();
}
cb();
});
}
};
async.series(tasks, function() {
console.log('Done');
});
Note: I'm doing a Q&A-style post and will be answering my own question.
After a lot of trail and error, it came down to how I was reading the file before being uploaded. As an artifact from a copy/paste, the encoding on the readFile function was kept. When I removed {'encoding':'UTF-8'} then uploaded the file, the resulting zip file was able to be unzipped just perfectly.
I simply removed the encoding on readFile, so with the changes the code now looks like this:
fs.readFile('my_file.zip', function(err, data) {
// ...
});

How to set custom header on form-data item?

I am sending a multipart data from a nodeJS route as a response.
I used form-data library to accomplish, my requirement is to send additional header information for all binary data.
I tried the following
router.get('/getdata', async (req, res, next) => {
var form = new FormData();
var encodedImage2 = fs.readFileSync('./public/image2.png');
var CRLF = '\r\n';
var options = {
header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF
};
form.append('image2.png', encodedImage2, options);
res.set('Content-Type', 'multipart/form-data; boundary=' + form.getBoundary());
form.pipe(res);
});
in the output I get only the header
X-Custom-Header: 123
without the option object I can get
Content-Disposition: form-data; name="image2.png"
Content-Type: application/octet-stream
I need an output headers something like
Content-Disposition: form-data; name="image2.png"
Content-Type: application/octet-stream
X-custom-Header: 123
I found the solution myself, and its very simple.
Its posible to set custom headers in the options object header property
router.get('/getdata', async (req, res, next) => {
var form = new FormData();
var encodedImage2 = fs.readFileSync('./public/image2.png');
var options = {
header: {
'X-Custom-Header': 123
}
};
form.append('image2.png', encodedImage2, options);
res.set('Content-Type', 'multipart/form-data; boundary=' + form.getBoundary());
form.pipe(res);
});
Just set your headers in express's res object like you do with the Content-Type:
router.get('/getdata', async (req, res, next) => {
var form = new FormData();
form.append('image2.png', fs.readFileSync('./public/image2.png'));
res.set('Content-Type', 'multipart/form-data; boundary=' + form.getBoundary());
res.set('X-Custom-Header', '123'); // Set header here
form.pipe(res);
});

Access files on Dropbox with NodeJS

I am using the Dropbox API with Node JS. I was able to upload files to my Dropbox using HTTP requests, but I am not able to download them with it. My intent is to use HTTP request to view content of the file in the dropbox.
This is the code for uploading files:
var request = require('request')
var fs = require('fs')
var token = "XXXXXXXXXXXXXXXXX"
var filename = "path/to/file/file.txt"
var content = fs.readFileSync(filename)
options = {
method: "POST",
url: 'https://content.dropboxapi.com/2/files/upload',
headers: {
"Content-Type": "application/octet-stream",
"Authorization": "Bearer " + token,
"Dropbox-API-Arg": "{\"path\": \"/files/"+filename+"\",\"mode\": \"overwrite\",\"autorename\": true,\"mute\": false}",
},
body:content
};
request(options,function(err, res,body){
console.log("Err : " + err);
console.log("res : " + res);
console.log("body : " + body);
})
Now what should the request function be for downloading this file? I was attempting something like this:
var request = require('request')
var fs = require('fs')
var token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
var filename = "path/to/file/file.txt"
var content = fs.readFileSync(filename)
options = {
method: "GET",
url: 'https://content.dropboxapi.com/2/files/upload',
headers: {
"Content-Type": "application/octet-stream",
"Authorization": "Bearer " + token,
"Dropbox-API-Arg": "{\"path\": \"/files/"+filename+"\",\"mode\": \"overwrite\",\"autorename\": true,\"mute\": false}",
},
};
request(options,function(err, res){
console.log("Err : " + err);
console.log("res : " + res);
})
But the res just gives object Object
How do I download the file?
You failed to download the file because the URL used(https://content.dropboxapi.com/2/files/upload) is incorrect. According to Dropbox API document, the correct URL endpoint is:
https://content.dropboxapi.com/2/files/download
However, it is better to use npm module such as dropbox to implement the requirement, as it has already wrapped the logic. The code would look like:
var fetch = require('isomorphic-fetch');
var Dropbox = require('dropbox').Dropbox;
var dbx = new Dropbox({ accessToken: 'YOUR_ACCESS_TOKEN_HERE', fetch: fetch });
dbx.filesDownload({path: '...'})
.then(function(data) {
...
});

Post multiple binary files with NodeJs

How can I POST multiple binary files to a server with Content-Type: "form-data", using only the http module?
For example, my keys would look like this:
{
Image1: <binary-data>,
Image2: <binary-data>
}
TL:DR; See the full example code at the bottom of this answer.
I was trying to figure out how to POST multiple binary image files to a server in NodeJS using only core NodeJs modules (without using anything like npm install request, etc). After about 3 hours typing the wrong search terms into DuckDuckGo and finding no examples online, I was about to switch careers. But after banging my head against the desk for almost half the day, my sense of urgency was dulled and I managed to hobble together a working solution. This would not have been possible without the good people who wrote the answer to this Stackoverflow post, and this Github Gist. I used PostMan and Charles Proxy to analyze successful HTTP POSTS, as I dug around in the NodeJS docs.
There are a few things to understand to POST two binary images and a text field as multipart/form-data, relying only on core NodeJS modules:
1) Boundary Identifiers
What is the boundary in multipart/form-data?
The first part of the solution is to create a "boundary identifier", which is a string of dashes -, appended with a random number. You could probably use whatever you wish, foorbar and such.
------------0123456789
Then place that boundary between each blob of data; whether that is binary data or just text data. When you have listed all your data, you add the boundary identifier at the end, and append two dashes:
------------0123456789--
You also need to add the boundary to the headers so that server receiving the post understands which lines of your post data form the boundary between fields.
const headers = {
// Inform the server what the boundary identifier looks like
'Content-Type': `multipart/form-data; boundary=${partBoundary}`,
'Content-Length': binaryPostData.length
}
2) Form Field Meta Descriptors
(This probably is not what they are called)
You will also need a way to write the meta data for each form-field you send, whether that form field contains a binary or a text object. Here are the descriptors for an image file, which contain the mime type:
Content-Disposition: form-data; name="Image1"; filename="image-1.jpg"
Content-Type: image/jpeg
And here is the descriptor for a text field:
Content-Disposition: form-data; name="comment"
The Post Data Output
So the entire post data that is sent to the server will look like this:
----------------------------9110957588266537
Content-Disposition: form-data; name="Image1"; filename="image-1.jpg"
Content-Type: image/jpeg
ÿØÿàJFIFHHÿáLExifMMi
ÿí8Photoshop 3.08BIM8BIM%ÔÙ²é ìøB~ÿÀ... <<<truncated for sanity>>>
----------------------------9110957588266537
Content-Disposition: form-data; name="Image2"; filename="image-2.jpg"
Content-Type: image/jpeg
ÿØÿàJFIFHHÿáLExifMMi
ÿí8Photoshop 3.08BIM8BIM%ÔÙ²é ìøB~ÿÀ... <<<truncated for sanity>>>
----------------------------9110957588266537
Content-Disposition: form-data; name="comment"
This is a comment.
----------------------------9110957588266537--
Once this post data is generated, it can be converted to binary and written to the HTTP POST request: request.write(binaryPostData).
Example Code
Here is the full example code that will allow you to POST binary file and text data without having to include other NodeJS libraries and packages in your code.
// This form data lists 2 binary image fields and text field
const form = [
{
name: 'Image1',
type: 'file',
value: 'image-1.jpg'
},
{
name: 'Image2',
type: 'file',
value: 'image-2.jpg'
},
{
name: 'comment',
type: 'text',
value: 'This is a comment.'
}
]
// Types of binary files I may want to upload
const types = {
'.json': 'application/json',
'.jpg': 'image/jpeg'
}
const config = {
host: 'ec2-192.168.0.1.compute-1.amazonaws.com',
port: '80',
path: '/api/route'
}
// Create an identifier to show where the boundary is between each
// part of the form-data in the POST
const makePartBoundary = () => {
const randomNumbers = (Math.random() + '').split('.')[1]
return '--------------------------' + randomNumbers
}
// Create meta for file part
const encodeFilePart = (boundary, type, name, filename) => {
let returnPart = `--${boundary}\r\n`
returnPart += `Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n`
returnPart += `Content-Type: ${type}\r\n\r\n`
return returnPart
}
// Create meta for field part
const encodeFieldPart = (boundary, name, value) => {
let returnPart = `--${boundary}\r\n`
returnPart += `Content-Disposition: form-data; name="${name}"\r\n\r\n`
returnPart += value + '\r\n'
return returnPart
}
const makePostData = {
// Generate the post data for a file
file: (item, partBoundary) => {
let filePostData = ''
// Generate the meta
const filepath = path.join(__dirname, item.value)
const extention = path.parse(filepath).ext
const mimetype = types[extention]
filePostData += encodeFilePart(partBoundary, mimetype, item.name, item.value)
// Add the binary file data
const fileData = fs.readFileSync(filepath, 'binary')
filePostData += fileData
filePostData += '\r\n'
return filePostData
},
// Generate post data for the text field part of the form
text: (item, partBoundary) => {
let textPostData = ''
textPostData += encodeFieldPart(partBoundary, item.name, item.value)
return textPostData
}
}
const post = () => new Promise((resolve, reject) => {
let allPostData = ''
// Create a boundary identifier (a random string w/ `--...` prefixed)
const partBoundary = makePartBoundary()
// Loop through form object generating post data according to type
form.forEach(item => {
if (Reflect.has(makePostData, item.type)) {
const nextPostData = makePostData[item.type](item, partBoundary)
allPostData += nextPostData
}
})
// Create the `final-boundary` (the normal boundary + the `--`)
allPostData += `--${partBoundary}--`
// Convert the post data to binary (latin1)
const binaryPostData = Buffer.from(allPostData, 'binary')
// Generate the http request options
const options = {
host: config.host,
port: config.port,
path: config.path,
method: 'POST',
headers: {
// Inform the server what the boundary identifier looks like
'Content-Type': `multipart/form-data; boundary=${partBoundary}`,
'Content-Length': binaryPostData.length
}
}
// Initiate the HTTP request
const req = http.request(options, res => {
res.setEncoding('utf8')
let body = ''
// Accumulate the response data
res.on('data', chunk => {
body += chunk
})
// Resolve when done
res.on('end', () => {
resolve(body)
})
res.on('close', () => {
resolve(body)
})
res.on('error', err => {
reject(err)
})
})
// Send the binary post data to the server
req.write(binaryPostData)
// Close the HTTP request object
req.end()
})
// Post and log response
post().then(data => {
console.log(data)
})
.catch(err => {
console.error(err)
})

Posting images to twitter in Node.js using Oauth

I'm trying to post images to Twitter using the Oauth module. Here is what I have:
It throws a 403 error, I know im doing something wrong with how I add the media to the post but Im just not sure where to go from here.
var https = require('https');
var OAuth= require('oauth').OAuth;
var keys = require('./twitterkeys');
var twitterer = new OAuth(
"https://api.twitter.com/oauth/request_token",
"https://api.twitter.com/oauth/access_token",
keys.consumerKey,
keys.consumerSecret,
"1.0",
null,
"HMAC-SHA1"
);
var params = {
status : "Tiger!",
media : [("data:" + mimeType + ";base64,") + fs.readFileSync(path,'base64')]
};
//function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback)
twitterer.post("https://upload.twitter.com/1/statuses/update_with_media.json",
keys.token, keys.secret, params, "multipart/form-data",
function (error, data, response2) {
if(error){
console.log('Error: Something is wrong.\n'+JSON.stringify(error)+'\n');
}else{
console.log('Twitter status updated.\n');
console.log(response2+'\n');
}
});
Here is what I belive im supose to be doing but I don't know how to do that in the Node.js Oauth module.
Posting image to twitter using Twitter+OAuth
Reviewing the code, it looks like there's no multipart/form-data handling at all in the node-oauth package right now. You can still use the node-oauth function to create the authorization header, but you'll have to do the multipart stuff on your own.
There are probably third-party libraries that can help out with that, but here's how I got it to work constructed by hand.
var data = fs.readFileSync(fileName);
var oauth = new OAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
twitterKey, twitterSecret,
'1.0', null, 'HMAC-SHA1');
var crlf = "\r\n";
var boundary = '---------------------------10102754414578508781458777923';
var separator = '--' + boundary;
var footer = crlf + separator + '--' + crlf;
var fileHeader = 'Content-Disposition: file; name="media"; filename="' + photoName + '"';
var contents = separator + crlf
+ 'Content-Disposition: form-data; name="status"' + crlf
+ crlf
+ tweet + crlf
+ separator + crlf
+ fileHeader + crlf
+ 'Content-Type: image/jpeg' + crlf
+ crlf;
var multipartBody = Buffer.concat([
new Buffer(contents),
data,
new Buffer(footer)]);
var hostname = 'upload.twitter.com';
var authorization = oauth.authHeader(
'https://upload.twitter.com/1/statuses/update_with_media.json',
accessToken, tokenSecret, 'POST');
var headers = {
'Authorization': authorization,
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Host': hostname,
'Content-Length': multipartBody.length,
'Connection': 'Keep-Alive'
};
var options = {
host: hostname,
port: 443,
path: '/1/statuses/update_with_media.json',
method: 'POST',
headers: headers
};
var request = https.request(options);
request.write(multipartBody);
request.end();
request.on('error', function (err) {
console.log('Error: Something is wrong.\n'+JSON.stringify(err)+'\n');
});
request.on('response', function (response) {
response.setEncoding('utf8');
response.on('data', function (chunk) {
console.log(chunk.toString());
});
response.on('end', function () {
console.log(response.statusCode +'\n');
});
});

Resources