Upload File Using NPM Request from new Formdata() - node.js

i'am develop backend using node-js and https://www.npmjs.com/package/request to handdle request to main api.
has successfully to send data in the form of string or a number. but I have a problem to send the file. before getting to the request module, i have convert all request from client using
new formdata()
end this is what i code using NPM request
export function requestAPI(method='GET', endpoint='', params={}, callback)
{
let token = ''
if(params.token)
{
token = params.token;
delete params.token;
}
//set query
if(params.query)
{
endpoint = `${endpoint}?${Url.serialize(params.query)}`
delete params.query
}
//set options
let options = {
method: method,
uri: `${process.env.API_HOST}${endpoint}`,
timeout: 6000,
headers: {
'auth' : token
},
};
// upload files
// ???
// using POST method
if(method === 'POST') {
options['form'] = params;
}
// is upload a file - request via multipart/form-data
//start request
try {
request( options , function(error, response, body){
if(error)
{
console.log(error)
return callback(httpException(500));
} else //success
{
return callback(JSON.parse(body));
}
})
} catch(err) {
return callback(httpException(500, err.message+', '+err.stack));
}
}

For sending files you will need to use something like multipart/form-data instead of application/json. In addition, you will need to use the formData option instead of form. For example:
var options = {
method: method,
uri: `${process.env.API_HOST}${endpoint}`,
timeout: 6000,
headers: {
'auth' : token,
},
};
// using POST method
if (method === 'POST') {
options.formData = params;
}
Then inside params you can use any values as outlined in the request and/or form-data modules' documentation. So for local files, you can just use a readable stream:
var fs = require('fs');
// ...
params.avatar = fs.createReadStream('avatar.jpg');
For files you can explicitly set a different filename and/or mime type as shown in the relevant request multipart/form-data example.

thanks for help guys, finaly i found the answer to solved it.
the problem on this line
if(method === 'POST') {
options['form'] = params;
}
i just change to this, to make it works
if(method === 'POST') {
options['formData'] = params;
}
and this is the complete of codes
export function requestAPI(method='GET', endpoint='', params={}, callback)
{
let token = ''
if(params.token)
{
token = params.token;
delete params.token;
}
//set query
if(params.query)
{
endpoint = `${endpoint}?${Url.serialize(params.query)}`
delete params.query
}
//set options
var options = {
method: method,
uri: `${process.env.API_HOST}${endpoint}`,
timeout: 30000,
headers: {
'auth' : token
},
};
// using POST method
if(method.toLowerCase() === 'post') {
options.formData = params;
// upload files
if(options.formData.files)
{
const files = options.formData.files
delete options.formData['files']
Object.keys(files).map(n => {
options.formData[n] = {
value: fs.createReadStream(files[n]._writeStream.path),
options: {
filename: files[n].name,
type: files[n].type
}
}
})
}
}
//start request
try {
request( options , function(error, response, body){
if(error)
{
return callback(httpException(500));
} else //success
{
return callback(JSON.parse(body));
}
})
} catch(err) {
return callback(httpException(500, err.message+', '+err.stack));
}
}

Related

Implementation of rxjs BehaviourSubject in Next.js for state management not working

Trying to store jwt token on login using rxjs behavioursubject
Then creating a http request with Authorization: Bearer ${user.jwtToken} in the
I believe I need to have
a) initial value,
b) a source that can be turned into an observable
c) a public variable that can be subscribed
On log in the user is correctly added to the user subject here "userSubject.next(user);"
But whenever I try to create the bearer token its always null
// The Accounts Service
// initialise and set initial value
const userSubject = new BehaviorSubject(null);
const authApiUrl = "https:testApi";
export const accountService = {
` user: userSubject.asObservable(), get userValue() { return userSubject.value },
login,
getAllUsers
};
function login(email, password) {
return fetchWrapper.post(process.env.AuthApiUrl + '/accounts/authenticate', { email, password })
.then(user => {
userSubject.next(user);
localStorage.setItem('user', JSON.stringify(user));
return user;
});
}
function getAllUsers() {
return await fetchWrapper.get(process.env.AuthApiUrl + '/accounts/get-all-users');
}
}
// The fetchwrapper
export const fetchWrapper = {
get,
post
};
function post(url, body) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeader(url) },
credentials: 'include',
body: JSON.stringify(body)
};
return fetch(url, requestOptions).then(handleResponse);
}
function get(url) {
const requestOptions = {
method: 'GET',
headers: authHeader(url)
};
return fetch(url, requestOptions).then(handleResponse);
}
function authHeader(url) {
// return auth header with basic auth credentials if user is logged in and request is to the api url
// THE accountService.userValue IS ALWAYS NULL???
const user = accountService.userValue;
const isLoggedIn = user && user.jwtToken;
const isApiUrl = url.startsWith(process.env.AuthApiUrl);
if (isLoggedIn && isApiUrl) {
return { Authorization: `Bearer ${user.jwtToken}` };
} else {
return {};
}
}
function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if ([401, 403].includes(response.status) && accountService.userValue) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
accountService.logout();
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}

Add custom header in FormData request

I have a Node function that uploads a file to an endpoint using FormData:
const uploadFile = (filepath) => {
return new Promise((resolve, reject) => {
const formData = new FormData();
// Add file
formData.append("file", fs.createReadStream(filepath));
formData.submit(`${API_URL}/v2/upload_file`, (error, res) => {
if (error) {
return reject(error);
}
if (res.statusCode !== 201) {
return reject(res.statusMessage);
}
return resolve(res);
});
});
};
This works fine. The problem is that after an update on the endpoint, a custom header with a token is now required:
'x-session-token': 'abc123'
I can't find a way to add this token. formData.append() only adds key-values to the body. I was reading the node source code but can't find a method or a way to add options like headers.
I also tried using fetch() adding the headers, but is not parsing the multipart/form-data body data in a valid format.
Any ideas?
From the documentation...
In case you need to also send custom HTTP headers with the POST request, you can use the headers key in first parameter of form.submit()
const url = new URL(`${API_URL}/v2/upload_file`);
formData.submit({
headers: { "x-session-token": "abc123" },
host: url.hostname,
path: url.pathname,
port: url.port,
protocol: url.protocol,
}, (error, res) => {
// ...
});
IMO this is pretty clunky, particularly having to parse the URL into its components. It also doesn't support query parameters.
I would prefer to use fetch()
const uploadFile = async (filepath) => {
const formData = new FormData();
// Add file
formData.append("file", fs.createReadStream(filepath));
const res = await fetch(`${API_URL}/v2/upload_file`, {
method: "POST",
body: formData,
headers: {
...formData.getHeaders(),
"x-session-token": "abc123",
},
});
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json(); // or res.text()
};

Download zip release from private repository with node.js

I'm trying to download zip release from a private repository, i've tried many solutions but none seems to work.
Here my code :
function Download(url, path, options) {
updateHeader(options.stageTitles.Downloading)
let received_bytes = 0;
let total_bytes = 0;
// eslint-disable-next-line
console.log('-----------------')
// eslint-disable-next-line
console.log(url, path)
console.log('-----------------')
var req = request(
{
method: 'GET',
uri: url,
headers: {
Authorization: `token ${options.gitRepoToken}`,
'Accept': 'application/octet-stream',
'User-Agent': 'request module',
},
//encoding: null
}
);
// eslint-disable-next-line
console.log(req)
var out = fs.createWriteStream(path);
req.pipe(out);
req.on('response', data => {
// eslint-disable-next-line
console.log(data.headers, data)
total_bytes = parseInt(data.headers['content-length']);
});
req.on('data', chunk => {
received_bytes += chunk.length;
showProgress(received_bytes, total_bytes);
});
req.on('end', () => {
Install(options)
});}
url variable equal something like : https://github.com/SolberLight/PRIVATE_REPO_NAME/releases/download/1.0/MY_ZIP_NAME.zip
Looks like I'm always getting a content size of 9 bytes with a "not found" response so I'm guessing that my headers aren't correct ?
thanks for your help !
EDIT 1 :
Made some progress but now it seems that the blob is not the right size
(async () => {
//get latest release
var response = await axios.get(
`https://api.github.com/repos/${repoWithOwner}/releases/latest`,
{
headers: authHeaders,
}
);
var assets = response.data.assets;
for (var i = 0; i < assets.length; i++) {
console.log(assets[i].url);
response = await axios({
method: "get",
url: assets[i].url,
responseType: "blob",
headers: {
//Accept: "application/octet-stream",
...authHeaders,
},
});
// eslint-disable-next-line
console.log(response.data, path)
let reader = new FileReader()
reader.onload = function() {
if (reader.readyState == 2) {
var buffer = new Buffer(reader.result)
console.log(`Saving ${JSON.stringify({ fileName: 'myfile.zip', size: response.data.size })}`)
outputFile(path, buffer, err => {
if (err) {
// eslint-disable-next-line
console.log(err.message)
} else {
// eslint-disable-next-line
console.log(path)
}
})
}
}
reader.readAsArrayBuffer(response.data)
}
})();
You can use the Download repository archive (zip) endpoint, documented here
Take into consideration the following:
You will perform a GET request to /repos/{owner}/{repo}/zipball/{ref}
The first request will send a redirect (302 response), you need to follow the redirect properly, in this case, since you're using axios, you will need to handle it manually. The response will include a zipball_url you will use in the following request.
Since you are downloading a private repo release, take into consideration these links are temporary and expire after five minutes.
The first request accepts a user or installation token (in case you're consuming the API as a GitHub Application). Read more about the difference between a GitHub App and a GitHub OAuth App here
If you are using a GitHub OAuth application, ensure you have the repo scope, for GitHub Application, ensure you have the Contents permissions
Check out the following working example:
Download a zip release from GitHub                                                                                
View in Fusebit
const owner = 'stack-overflow-demos';
// For the purpose of this example, we have 1 release, so it will be always the first item.
const releases = await installationClient.rest.repos.listReleases({
owner: 'stack-overflow-demos',
repo: 'private-demo'
});
// Get release information (e.g zipball URL)
const release = await installationClient.rest.repos.getRelease({
owner: 'stack-overflow-demos',
repo: 'private-demo',
release_id: releases.data[0].id,
});
const { token } = await installationClient.auth();
// According to official GitHub docs https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-zip
// You will get a zipball URL you can use to download the zip file, you need to handle properly the redirects since a second GET
// request is needed, this time with an authenticated URL.
const { zipball_url, tag_name } = release.data;
const response = await getZipFile(zipball_url, token);
// Handle redirects properly if a responseUrl is returned that means you need to perform the redirect,
// you don't need to send the access token to this request, since is a signed URL
// Note: For private repositories, these links are temporary and expire after five minutes.
const {
request: {
res: { responseUrl },
},
} = response;
if (responseUrl) {
await downloadZipFile(responseUrl, `/tmp/'${tag_name}.zip`);
}
// Get information about the zip file you're about to download
const getZipFile = async (url, token) => {
return await axios({
redirect: 'manual',
method: 'get',
url,
headers: {
Authorization: `token ${token}`,
Accept: 'application/vnd.github.v3+json',
},
});
};
// Download the zip file from the temporary secure URL returned from the first request
const downloadZipFile = async (url, filePath) => {
const response = await axios({
method: 'get',
url,
responseType: 'stream',
headers: {
Accept: 'application/octet-stream',
},
});
return new Promise((resolve, reject) => {
console.log(`writing to ${filePath}`);
const dest = fs.createWriteStream(filePath);
let progress = 0;
response.data
.on('end', () => {
console.log('🎉 Done downloading file.');
resolve(filePath);
})
.on('error', (err) => {
console.error('🚫 Error downloading file.');
reject(err);
})
.on('data', (d) => {
progress += d.length;
console.log(`🕛 Downloaded ${progress} bytes`);
})
.pipe(dest);
});
};
This example is using Octokit

Why does fs.readFile return null for first fetch call?

I have a simple function to return a pdf file on my server which works as expected but not the first time the page loads. The first time I try to download the file, the server console log shows me it's there, but the React page console has null. If I click on the download link again, the file contents are returned. Can someone help me please?
React function that calls getDocByLoc (which is itself called by a button click)
async function fetchInvoice(fileLocation) {
setInvoiceStatus('loading');
setInvoice(await invoiceService.getDocByLoc({ location: fileLocation }));
setInvoiceStatus('succeeded');
downloadFile(invoice);
}
// download file
function downloadFile(invoice) {
if (invoiceStatus === 'succeeded') {
let url = window.URL.createObjectURL(invoice);
let a = document.createElement('a');
a.href = url;
a.download = 'test.pdf';
a.click();
}
}
React fetch code
function getDocByLoc(location) {
return fetchWrapper.getDocument(`/invoices/document`, location);
}
fetchWrapper code
function getDocument(url, location) {
const requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json, application/x-www-form-urlencoded',
'Content-Type': 'application/json',
...authHeader(url),
},
credentials: 'include',
body: JSON.stringify(location),
};
return fetch(`${AppSettings.serverEndpoint}${url}`, requestOptions).then(
handleResponseForDocuments
);
}
Node file reader code
function getDocumentByLocation(req, res, next) {
const { location } = req.body;
fs.readFile(location, (err, data) => {
if (err) res.status(500).send(err);
console.log('data: ', data);
res
.contentType('application/pdf')
.send(
`data:application/pdf;base64,${new Buffer.from(data).toString(
'base64'
)}`
);
});
}

Uploading blob/file in react-native, contents is empty

I am able to succesfully upload a blob with proper contents from my web browser, but when I do it from react-native, the upload file is empty. Here is the code:
async function doit() {
const data = new FormData();
data.append('str', 'strvalue');
data.append(
'f',
new File(['foo'], 'foo.txt', {type: 'text/plain'}),
);
await fetch('http://localhost:3002/upload', {
method: 'POST',
body: data
});
}
However doing this same code from react-native, it uploads, but the file is empty.
Here is the node.js server I am using to test this. Loading http://localhost:3002 gives you a button called "upload it". Clicking it does the upload from the web. Screenshots of results are below.
var multiparty = require('multiparty');
var http = require('http');
http
.createServer(function (req, res) {
if (req.url === '/upload' && req.method === 'POST') {
console.log('multipart here');
var form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
console.log(require('util').inspect({ fields, files }, false, null, true));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ bar: true }));
});
return;
}
console.log('here');
// show a file upload form
res.writeHead(200, { 'content-type': 'text/html' });
res.end(
`
<script>
async function doit() {
const data = new FormData();
data.append('str', 'strvalue');
data.append(
'f',
// new File([new Blob(['asdf'], {type : 'text/plain'})], 'filename.txt'),
new File(['foo', 'what', 'the', 'hell'], 'foo.txt', {type: 'text/plain'}),
);
const res = await fetch('http://localhost:3002/upload', {
method: 'POST',
body: data
});
console.log(JSON.stringify(res, null, 4));
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('b').addEventListener('click', doit, false)
}, false);
</script>
<button type="button" id="b">upload it</button>
`
);
})
.listen(3002);
From web browser we see the node server logs this, notice file size is 14.
However from react-native we see file size is 0:
I faced the same problem recently while posting an image from a react-native app to a server. However, I was able to make it work by appending the name and type of the file to the formData instance.
Here, the uri argument to uploadImageAsync is passed as a route parameter from the previous screen.
const postShoutHandler = async () => {
setShoutUploadStatus("Started Upload");
const response = await uploadImageAsync(route.params.captures);
const uploadResult = await response.json();
if (uploadResult === "Upload successful") {
setShoutUploadStatus("Success");
navigation.navigate("Home");
} else {
setShoutUploadStatus("Failed");
}
};
/* <--Upload image function --> */
const uploadImageAsync = (uri: string) => {
const apiUrl = "https://www.yourserver.com/image";
let uriParts = uri.split(".");
let fileType = uriParts[uriParts.length - 1];
let formData = new FormData();
formData.append("img", {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});
formData.append("description", "HEY");
let options = {
method: "POST",
body: formData,
headers: {
Accept: "application/json",
"Content-Type": "multipart/form-data",
Authorization: "Bearer " + accessToken,
},
};
return fetch(apiUrl, options);
};
/* <--Upload image function --> */
Here is the Image configuration.
const photoData = await camera.takePictureAsync({
base64: true,
exif: false,
});

Resources