Google Drive Api v3 get download progress - node.js

I am trying to get a download progress % for a huge file stored in my Google Drive unit when downloading from my Nodejs script.
So far I have written the code to download, which is working, however the on('data'....) part is never called.
const downloadFile = (file) => {
const fileId = file.id;
const fileName = path.join(basePathForStorage, file.name);
const drive = google.drive({ version: 'v3', authorization });
let progress = 0;
return new Promise((resolve, reject) => {
drive.files.get(
{
auth: authorization,
fileId: fileId,
alt: 'media'
},
{ responseType: "arraybuffer" },
function (err, { data }) {
fs.writeFile(fileName, Buffer.from(data), err => {
// THIS PART DOES NOTHING
data.on('data',(d)=>{
progress += d.length;
console.log(`Progress: ${progress}`)
})
// --------------------
if (err) {
console.log(err);
return reject(err);
}
return resolve(fileName)
});
}
);
});
}
Looks like I can't find the way to show the progess of the download by calling on('data'....)...wondering now if this is the correct way to do this, or if this is even possible.
I tried putting the on('data'....) code as it is now inside the writeFile function but also inside the callback from drive.files.get and nothing works.

Here it comes some code sample to do that,
this example has three parts that need to be mentioned:
Create a stream to track our download progress
Create a method to get the file size
Create an event emitter to send back our progress to our FE
So we will get the following:
const downloadFile = async(file) => {
const fileId = file.id
const fileName = path.join(basePathForStorage, file.name)
let progress = 0
/**
* ATTENTION: here you shall specify where your file will be saved, usually a .temp folder
* Here we create the stream to track our download progress
*/
const fileStream = fs.createWriteStream(path.join(__dirname, './temp/', filename))
const fileSize = await getFileSize(file)
// In here we listen to the stream writing progress
fileStream.on('data', (chunk) => {
progress += chunk.length / fileSize
console.log('progress', progress)
})
const drive = google.drive({
version: 'v3',
authorization
})
drive.files.get({
auth: authorization,
fileId: fileId,
alt: 'media'
}, {
responseType: "stream"
},
(err, { data }) =>
data
.on('end', () => console.log('onCompleted'))
.on('error', (err) => console.log('onError', err))
.pipe(fileStream)
)
}
The method to retrieve the file size:
const getFileSize = ({ fileId: id }) => {
const drive = google.drive({
version: 'v3',
authorization
})
return new Promise((resolve, reject) =>
drive.files.get({
auth: authorization,
fileId
}, (err, metadata) {
if (err) return reject(err)
else resolve(metadata.size)
})
}
This code sample give you the ability to get partial updates from your file download as you're creating a write stream (nodejs#createWriteStream)
So you will be able to track your file downloading progress.
But, still you have to continuosly send these changes to your client ( FE ).
So, you could create your own EventEmitter to track that.
And now our sample will be enchanced with the following:
In our endpoint:
import { EventEmitter } from 'events'
router.post('/myEndpoint', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
const progressEvent = new EventEmitter()
progressEvent.on('progress', (progress) => {
if (progress === 100)
res.end()
// So, your FE side will be receiving this message continuosly
else res.write(`{ progress: ${ progress } }`)
})
const file = req.body // or where you're getting your file from
downloadFile(file, progressEvent)
})
In our download method:
const downloadFile = async(file, progressEvent) => {
.
.
.
fileStream.on('data', (chunk) => {
progress += chunk.length / fileSize
progressEvent.emit('progress', progress)
.
.
.

Related

Download file from third party server and upload to S3

I have a Lambda Node function which is called by a webhook from a thirdparty server. The TP server sends a file download URL and some other data.
The download URL is temporary, so I need to push the file to an S3 for long term storage.
The rudimentary function below, downloads the file and then tries to upload to the S3.
This works when the file is a plain text, but images/pdfs etcs are corrupted when they reach the S3.
const AWS = require("aws-sdk");
const https = require('https');
const path = require('path');
const s3 = new AWS.S3({apiVersion: '2006-03-01'});
exports.handler = async (event, context, callback) => {
var payload = event.body;
const url_host = payload.host;
const url_path = payload.path; //URL of file which needs to be downloaded
const get_params = {
host: url_host,
path: url_path,
port: 443,
method: 'GET',
headers: { }
};
var resp = await https_get_processor(get_params); //File downloaded here
var uploadParams = {
Bucket: "bucket_name",
Key: '',
Body: resp //Preparing to upload the received file
};
uploadParams.Key = path.basename(url_path); //Generating filename
s3.upload (uploadParams, function (err, data) {
if (err) {
console.log("Error", err);
} if (data) {
console.log("Upload Success", data.Location);
}
});
response = {...} //Generic Response
return response;
};
async function https_get_processor(get_params)
{
return await new Promise((resolve, reject) =>
{
var data = "";
const req = https.request(get_params, res => {
res.on('data', chunk => { data += chunk })
res.on('end', () =>
{
resolve(data);
})
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
Response is a Buffer in such case, so try changing request processing by pushing each chunk into an array, and then merge Buffer chunks and pass them.
Try this:
var data = [];
const req = https.request(get_params, res => {
res.on('data', chunk => data.push(chunk))
res.on('end', () =>
{
resolve(Buffer.concat(data));
})

How to partially download video/image from Axios?

I have a NodeJS app and want to let user upload image/video from a given URL using Axios. But before downloading the media and store it on S3, I need to verify the width & height through ffprobe, and size of the media (can use Content-Length).
My current implementation is to get first 5 chunks and pipe it to write stream, but the video seems to be broken...
router.post('/upload', async (req, res) => {
const { file } = req.body;
try {
const rawPath = path.resolve(`./.temp/raw/${Math.random()}`);
const response = await axios.get(file, { responseType: 'stream' });
// Progress
const stream = response.data;
const chunks: Buffer[] = [];
const writeStream = fs.createWriteStream(rawPath);
let progress = 0;
await new Promise((resolve, reject) => {
stream.on('error', reject).once('close', () => resolve(rawPath));
stream.on('data', (data: Buffer) => {
progress += Buffer.byteLength(data);
console.log(progress);
chunks.push(data);
if (chunks.length >= 5) {
stream.destroy();
resolve(rawPath);
}
});
});
chunks.forEach((chunk) => {
writeStream.write(chunk);
});
writeStream.end();
res.json('ok');
} catch (e) {
console.log(e);
res.sendStatus(400);
}
});
Describe what you tried:
Axios piping to write stream, but can't seem to get partial media
What you expected to happen:
Get some chunks of video to be checked
What actually resulted:
Broken media after downloaded

how to upload file in ejs

Uploading file and get the formdata in nodejs server as seen below:
now all I need is post this data to remote API,
As you see in the image, all props are fine except the uploaded file. What should I do?
nodejsAPI:
async (req, res) => {
var form = new formidable.IncomingForm();
var params = {}
form.parse(req, async (err, fields, files) => {
Object.keys(fields).forEach(function(name) {
params[name] = fields[name]
});
params['MyFile'] = files['MyFile']
const result = await AdManagementService.createAdvertisement(params)
});
}
createAdvertisement action:
const createAdvertisement = async ({
Type,
MyFile,
}) => {
try {
const response = await axios.post(
`/ad/upload-file?Type=${Type}`,
{
data:MyFile,//if this line removed, it works fine..
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
console.log(response)
return response.data
} catch (error) {
return error
}
}
it works fine if data:MyFile is removed, but I need send the file as well, what should I do?
It returns 400

Multiple file upload to S3 with Node.js & Busboy

I'm trying to implement an API endpoint that allows for multiple file uploads.
I don't want to write any file to disk, but to buffer them and pipe to S3.
Here's my code for uploading a single file. Once I attempt to post multiple files to the the endpoint in route.js, it doesn't work.
route.js - I'll keep this as framework agnostic as possible
import Busboy from 'busboy'
// or const Busboy = require('busboy')
const parseForm = async req => {
return new Promise((resolve, reject) => {
const form = new Busboy({ headers: req.headers })
let chunks = []
form.on('file', (field, file, filename, enc, mime) => {
file.on('data', data => {
chunks.push(data)
})
})
form.on('error', err => {
reject(err)
})
form.on('finish', () => {
const buf = Buffer.concat(chunks)
resolve({
fileBuffer: buf,
fileType: mime,
fileName: filename,
fileEnc: enc,
})
})
req.pipe(form)
})
}
export default async (req, res) => {
// or module.exports = async (req, res) => {
try {
const { fileBuffer, ...fileParams } = await parseForm(req)
const result = uploadFile(fileBuffer, fileParams)
res.status(200).json({ success: true, fileUrl: result.Location })
} catch (err) {
console.error(err)
res.status(500).json({ success: false, error: err.message })
}
}
upload.js
import S3 from 'aws-sdk/clients/s3'
// or const S3 = require('aws-sdk/clients/s3')
export default (buffer, fileParams) => {
// or module.exports = (buffer, fileParams) => {
const params = {
Bucket: 'my-s3-bucket',
Key: fileParams.fileName,
Body: buffer,
ContentType: fileParams.fileType,
ContentEncoding: fileParams.fileEnc,
}
return s3.upload(params).promise()
}
I couldn't find a lot of documentation for this but I think I've patched together a solution.
Most implementations appear to write the file to disk before uploading it to S3, but I wanted to be able to buffer the files and upload to S3 without writing to disk.
I created this implementation that could handle a single file upload, but when I attempted to provide multiple files, it merged the buffers together into one file.
The one limitation I can't seem to overcome is the field name. For example, you could setup the FormData() like this:
const formData = new FormData()
fileData.append('file[]', form.firstFile[0])
fileData.append('file[]', form.secondFile[0])
fileData.append('file[]', form.thirdFile[0])
await fetch('/api/upload', {
method: 'POST',
body: formData,
}
This structure is laid out in the FormData.append() MDN example. However, I'm not certain how to process that in. In the end, I setup my FormData() like this:
Form Data
const formData = new FormData()
fileData.append('file1', form.firstFile[0])
fileData.append('file2', form.secondFile[0])
fileData.append('file3', form.thirdFile[0])
await fetch('/api/upload', {
method: 'POST',
body: formData,
}
As far as I can tell, this isn't explicitly wrong, but it's not the preferred method.
Here's my updated code
route.js
import Busboy from 'busboy'
// or const Busboy = require('busboy')
const parseForm = async req => {
return new Promise((resolve, reject) => {
const form = new Busboy({ headers: req.headers })
const files = [] // create an empty array to hold the processed files
const buffers = {} // create an empty object to contain the buffers
form.on('file', (field, file, filename, enc, mime) => {
buffers[field] = [] // add a new key to the buffers object
file.on('data', data => {
buffers[field].push(data)
})
file.on('end', () => {
files.push({
fileBuffer: Buffer.concat(buffers[field]),
fileType: mime,
fileName: filename,
fileEnc: enc,
})
})
})
form.on('error', err => {
reject(err)
})
form.on('finish', () => {
resolve(files)
})
req.pipe(form) // pipe the request to the form handler
})
}
export default async (req, res) => {
// or module.exports = async (req, res) => {
try {
const files = await parseForm(req)
const fileUrls = []
for (const file of files) {
const { fileBuffer, ...fileParams } = file
const result = uploadFile(fileBuffer, fileParams)
urls.push({ filename: result.key, url: result.Location })
}
res.status(200).json({ success: true, fileUrls: urls })
} catch (err) {
console.error(err)
res.status(500).json({ success: false, error: err.message })
}
}
upload.js
import S3 from 'aws-sdk/clients/s3'
// or const S3 = require('aws-sdk/clients/s3')
export default (buffer, fileParams) => {
// or module.exports = (buffer, fileParams) => {
const params = {
Bucket: 'my-s3-bucket',
Key: fileParams.fileName,
Body: buffer,
ContentType: fileParams.fileType,
ContentEncoding: fileParams.fileEnc,
}
return s3.upload(params).promise()
}

Downloading an image from Drive API v3 continuously gives corrupt images. How should I decode the response from the promise?

I'm trying to download images from a Google share Drive using the API v3. The download itself will succeed but the image can't be seen. Opening the image from the MacOS finder just results in a spinner.
I started using the example from the documentation (here: https://developers.google.com/drive/api/v3/manage-downloads):
const drive = google.drive({version: 'v3', auth});
// ....
var fileId = '0BwwA4oUTeiV1UVNwOHItT0xfa2M';
var dest = fs.createWriteStream('/tmp/photo.jpg');
drive.files.get({
fileId: fileId,
alt: 'media'
})
.on('end', function () {
console.log('Done');
})
.on('error', function (err) {
console.log('Error during download', err);
})
.pipe(dest);
however that fails because the .on() method doesn't exist. The exact error is "TypeError: drive.files.get(...).on is not a function"
The .get() method returns a promise. The response of the promise contains data that, depending on the config is either a stream, a blob or arraybuffer. For all options, when I write the response data to a file, the file itself becomes unviewable and has the wrong size. The actual code (typescript, node.js) for the arraybuffer example is below. Similar code for blob (with added name and modifiedDate) and for stream give the same result.
const downloader = googleDrive.files.get({
fileId: file.id,
alt: 'media',
}, {
responseType: 'arraybuffer',
});
return downloader
.then((response) => {
const targetFile = file.id + '.' + file.extension;
fs.writeFileSync(targetFile, response.data);
return response.status;
})
.catch((response) => {
logger.error('Error in Google Drive service download: ' + response.message);
return response.message;
}
);
}
So the questions are:
what is the correct way to handle a download through Google Drive API v3 ?
do I need to handle any formatting of the response data ?
All help greatly appreciated!
Thanks
You want to download a file from Google Drive using googleapis with Node.js.
You have already been able to use Drive API.
If my understanding is correct, how about this answer?
Pattern 1:
In this pattern, arraybuffer is used for responseType.
Sample script:
const drive = google.drive({ version: "v3", auth });
var fileId = '###'; // Please set the file ID.
drive.files.get(
{
fileId: fileId,
alt: "media"
},
{ responseType: "arraybuffer" },
function(err, { data }) {
fs.writeFile("sample.jpg", Buffer.from(data), err => {
if (err) console.log(err);
});
}
);
In this case, Buffer.from() is used.
Pattern 2:
In this pattern, stream is used for responseType.
Sample script:
const drive = google.drive({ version: "v3", auth });
var fileId = '###'; // Please set the file ID.
var dest = fs.createWriteStream("sample.jpg");
drive.files.get(
{
fileId: fileId,
alt: "media"
},
{ responseType: "stream" },
function(err, { data }) {
data
.on("end", () => {
console.log("Done");
})
.on("error", err => {
console.log("Error during download", err);
})
.pipe(dest);
}
);
Note:
If an error occurs, please use the latest version of googleapis.
From your question, it seems that you have already been able to retrieve the file you want to download using your request, while the file content cannot be opened. But if an error occurs, please try to add supportsAllDrives: true and/or supportsTeamDrives: true in the request.
References:
Download files
google-api-nodejs-client/samples/drive/download.js
If I misunderstood your question and this was not the direction you want, I apologize.
Posting a third pattern for completeness using async/await and including teamdrive files.
async function downloadFile(drive: Drive, file: Schema$File, localDir: string = "/tmp/downloads") {
if (!fs.existsSync(localDir)) {
fs.mkdirSync(localDir)
}
const outputStream = fs.createWriteStream(`${localDir}/${file.name}`);
const { data } = await drive.files.get({
corpora: 'drive',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
fileId: file.id,
alt: "media",
}, {
responseType: 'stream',
})
await pipeline(data, outputStream)
console.log(`Downloaded file: ${localDir}/${file.name}`)
}
If someone is looking for a solution is 2023, here you go!
const downloadFile = async (file) => {
const dirPath = path.join(process.cwd(), '/images');
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const filePath = `${dirPath}/${file.name}.jpg`;
const destinationStream = fs.createWriteStream(filePath);
try {
const service = await getService();
const { data } = await service.files.get(
{ fileId: file.id, alt: 'media' },
{ responseType: 'stream' }
);
return new Promise((resolve, reject) => {
data
.on('end', () => {
console.log('Done downloading file.');
resolve(filePath);
})
.on('error', (err) => {
console.error('Error downloading file.');
reject(err);
})
.pipe(destinationStream);
});
} catch (error) {
throw error;
}
};

Resources