Download zip release from private repository with node.js - 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

Related

nodejs modio api "add modfile" failing to upload

Im trying to upload modfiles with the api but it keeps saying that Im not including filedata. Ive tried with fetch like in the docs but it just gives the same error. If I try http it just gives a list of the files as if it was a GET request.
var zip = `./mod.zip`; // it exists!
var body = {
//filedata: `#${zip}`,
filedata: fs.readFileSync(zip, `binary`),
//filehash: crypto.createHash('md5').update(fs.readFileSync(zip, `binary`)).digest('hex'),
//version: version,
//active: active,
//changelog: changelog,
//metadata_blob: meta,
};
var res = await fetch(`https://api.mod.io/v1/games/${config.modio.gameid}/mods/${config.modio.modid}/files`, { // actually HTTP
method: 'POST',
headers: {
'Authorization': `Bearer ${config.modio.token}`,
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
},
body: JSON.stringify(body),
});
//console.log(body.filedata);
res = await res.json();
if (res.error)
console.log(res.error);
else
console.log(res);
{
"error": {
"code": 422,
"error_ref": 13009,
"message": "Validation Failed. Please see below to fix invalid input:",
"errors": {
"filedata": "The filedata field is required when upload id is not present.",
"upload_id": "The upload id field is required when filedata is not present."
}
}
}
Yes, ive submitted a bug report to them already, twice. Now they are ghosting me. (I'll probably submit a link to this as well)
They replied!
Hi
As mentioned, you would need to send data using FormData, not JSON.
You can find details at https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
Thanks,
Danny King
Developer Relations Specialist
So, I remade my code
const https = require(`https`);
var crypto = require('crypto');
var FormData = require('form-data');
function ObjectToForm(obj = {}) {
var form = new FormData();
Object.keys(obj).forEach(key => {
var val = obj[key];
switch (typeof val) {
case `boolean`:
val = String(val);
break;
}
form.append(key, val);
});
return form;
}
if (fs.statSync(zip).size > 5368709120) return consolelog(`Zip bigger then 5gb`);
var body = {
filedata: fs.createReadStream(zip),
filehash: crypto.createHash('md5').update(fs.readFileSync(zip)).digest('hex'),
version: version,
active: active,
changelog: changelog,
metadata_blob: meta,
};
var form = ObjectToForm(body);
var options = {
hostname: 'api.mod.io',
port: 443,
path: `/v1/games/${config.modio.gameid}/mods/${config.modio.modid}/files`,
method: 'POST',
headers: {
'Authorization': `Bearer ${config.modio.token}`,
...form.getHeaders(),
'Accept': 'application/json',
},
};
var req = https.request(options, (res) => {
var data = [];
res.on('data', (d) => data.push(d));
req.on(`close`, () => {
var buffer = Buffer.concat(data);
var resp = JSON.parse(buffer.toString());
if (resp.error)
console.log(resp.error);
else if (res.statusCode == 201)
r(true);
});
});
req.on('error', (e) => {
console.log(`Error publishing:`);
console.log(e);
r(e);
});
form.pipe(req)
.on(`close`, () => req.end());
And it worked. Thanks King.

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()
};

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'
)}`
);
});
}

React.js download pdf file from node backend

I have code that just downloads a file that exists in my backend. I can see the pdf on the backend is created properly and in the right place, but when I send and download the file to the frontend and open it I get the error "Failed to load pdf document" no matter which browser I use. I think this must mean there is something wrong with my blob downloading code since I can open and see the file on the backend, but I can't figure it out. I have tried following many examples online and I get the same issue regardless of what I have tried.
server.js (node backend file)
app.get('/api/v1/getPdf', function(req, res) {
let resolve = require('path').resolve
res.sendFile(resolve('./tickets/tickets.pdf'));
});
PrintDetails.js (React js code for downloading pdf) - Note: I only included the relevant parts
class PrintDetails extends React.Component {
async printTickets() {
let file = await getTicketsPdf();
console.log(file);
const blob = new Blob([file], {type: 'application/pdf'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'tickets.pdf';
document.body.appendChild(link);
link.click();
setTimeout(function() {
document.body.removeChild(link);
}, 100);
}
render() {
return (
<div>
<button className="download-button-icon" onClick={this.printTickets}>Download</button>
</div>
)
}
async function getTicketsPdf() {
let data = {};
await (async () => {
const rawResponse = await fetch('/api/v1/getPdf', {
method: 'get',
headers: {
'responseType': 'blob',
'Content-Type': 'application/json'
},
});
data = await rawResponse;
})();
return data;
}
Here's my implementation using axios and file-saver.
Node.js backend
app.get('/api/v1/getPdf', function(req, res) {
res.download('./tickets/tickets.pdf');
});
React frontend
import { saveAs } from 'file-saver'
.
.
.
async function printTickets() {
const { data } = await getTicketsPdf()
const blob = new Blob([data], { type: 'application/pdf' })
saveAs(blob, "tickets.pdf")
}
async function getTicketsPdf() {
return axios.get('/api/v1/getPdf', {
headers: {
'Content-Type': 'multipart/form-data'
},
responseType: 'arraybuffer'
})
}

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