I'm trying to upload a file to S3 using the node-formidable method fileWriteStreamHandler.Before the upload to S3 I want to create a hash of file. This means implementing a stream pipe that first pass the data through a hash then passes that data to the S3 upload.
When trying to implement the pipe I kept running into issues. So below is a simplified function that more or less represents what I want to do.
formHandler.js
const form = formidable({
encoding: 'utf-8',
keepExtensions: true,
allowEmptyFiles: false,
maxFiles, maxFileSize, maxTotalFileSize,
maxFields, maxFieldsSize, minFileSize,
multiples: true,
fileWriteStreamHandler: streamUploadImage,
});
streamUploadImage.js
function streamUploadImage() {
const firstStream = new PassThrough();
const lastStream = new PassThrough();
const hash = createHash('SHA2-256');
hash.setEncoding('hex');
const transform = new Transform({
transform(chunk, encoding, cb) {
hash.write(chunk);
cb();
},
flush(cb) {
hash.end();
console.log('all done', hash.read());
cb();
}
});
firstStream.on('data', () => console.log('first'));
lastStream.on('data', () => console.log('last'));
return first.pipe(transform).pipe(last);
};
When using the above streamUploadImage only the lastStream is called. firstStream & transform are never called.
Why is that? Is the pipeline not implemented correctly? Does the formidable fileWriteStreamHandler not work with pipes?
using formidable#3.2.1
UPDATE:
see below for a quick reproduction of my issue:
var server = http.createServer(function (req, res) {
if (req.url == '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<form action="/upload" enctype="multipart/form-data" method="post">
<label>file name<input type="text" name="file_name" autofocus /></label><br />
<label>single file<input type="file" name="file_single" /></label><br />
<label>multiple files<input type="file" name="filearray_with_multiple[]" multiple /></label><br />
<br />
<button>Upload</button>
</form>
`);
res.end();
} else if (req.url === '/upload') {
const form = formidable({
encoding: 'utf-8',
keepExtensions: true,
allowEmptyFiles: false,
multiples: true,
fileWriteStreamHandler: streamUploadImage,
});
form.parse(req, (err, fields, files) => {
if (err) throw err;
console.log('parsed file upload');
console.log({ fields, files });
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ err, fields, files}, null, 2))
})
}
});
function streamUploadImage() {
const firstStream = new PassThrough();
const lastStream = new PassThrough();
const hash = createHash('SHA2-256');
hash.setEncoding('hex');
const transform = new Transform({
transform(chunk, encoding, cb) {
hash.write(chunk);
cb();
},
flush(cb) {
hash.end();
console.log('all done', hash.read());
cb();
}
});
firstStream.on('data', () => console.log('first'));
lastStream.on('data', () => console.log('last'));
return firstStream.pipe(transform).pipe(lastStream);
};
server.listen(5000);
stream.pipe() returns the destination stream to allow for chaining.
You need to return the head of the pipeline from streamUploadImage() (firstStream in your example), rather than the tail.
function streamUploadImage() {
const firstStream = new PassThrough();
const lastStream = new PassThrough();
// *snip*
// Wire up the pipeline
firstStream.pipe(transform).pipe(lastStream);
// Return the head of the pipeline
return firstStream;
};
Related
I am trying to stream a file to S3 without storing the file to disk/ssd. I would like to have part of the hash of the file as a part of the filename when uploading to S3.
EDIT_v1:
Been trying to follow this post using busboy as the parser: Calculate a file hash and save the file. I took an example from the busboy docs and adabpted it with an answer from the post:
const server = http.createServer();
server.on('request', async (req, res) => {
if (req.method === 'POST') {
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
const { filename, encoding, mimeType } = info;
console.log(
`File [${name}]: filename: %j, encoding: %j, mimeType: %j`,
filename,
encoding,
mimeType
);
const fileHashSource = new PassThrough();
const writeSource = new PassThrough();
file.pipe(fileHashSource);
file.pipe(writeSource);
fileHashSource.resume();
writeSource.resume();
createFileHash(fileHashSource, (err, hash) => {
if (err) {
console.log('err', err)
return res.end('some err');
}
const writeStream = fs.createWriteStream(`test_${hash.slice(0, 8)}.png`);
writeStream.on('error', function(err) {
console.log('write error', err);
return res.end('write error')
});
writeStream.on('finish', function() {
console.log('write finished')
return res.end('done')
});
writeSource.pipe(writeStream);
});
});
bb.on('field', (name, val, info) => {
console.log(`Field [${name}]: value: %j`, val);
});
bb.on('close', () => {
console.log('Done parsing form!');
req.unpipe(bb);
res.writeHead(201, { Connection: 'close' });
res.end('done!');
});
req.pipe(bb);
} else if (req.method === 'GET') {
res.writeHead(200, { Connection: 'close' });
res.end(`
<body style="background-color: black">
<form enctype="multipart/form-data" method="post">
<label>file name
<input type="text" name="textfield" />
</label><br />
<label>single file
<input type="file" name="filefield" />
</label><br />
<br />
<button type="submit">Upload</button>
</form>
</body>
`);
}
})
server.listen(3000, () => {
console.info(`NodeJS process: ${process.pid}`)
console.info(`Listening on port: 3000`)
});
function createFileHash(readStream, next) {
const hash = crypto.createHash('sha1');
hash.setEncoding('hex');
hash.on('error', function(err) {
console.log('hash error')
return next(err);
});
hash.on('finish', function(data) {
console.log('hash finished');
return next(null, hash.read());
});
readStream.pipe(hash);
}
EDIT_v2:
see first answer below for a solution
I put the task flow in a pipeline, implemented late piping with PassThrough, and finally used a function that returns an async generator that uploads to S3
const { fileStream, mimeType } = createFromBusBoy();
const s3Source = new PassThrough();
filestream.on('data', chunk => {
s3Source.write(chunk);
});
filestream.on('end', () => {
s3Source.end();
});
const hash = createHash('sha256');
hash.setEncoding('hex');
try {
await pipeline(
filestream,
hash,
uploadImage(s3Source, mimeType),
);
} catch (err) {
console.log(err)
throw err;
}
function uploadImage(fileStream, mimeType) {
return async function* (source, signal) {
let hash;
for await (const chunk of source) {
hash = chunk;
}
yield await uploadToS3(filestream, hash, mimeType);
};
}
hi guys i try to upload file from react-nodejs to google clode storage ,
in client when i upload file and console log the file , it show the file array but when i try to send to server side , the array is empty
this is client
const [myFile, setmyFile] = useState("");
const onFileUpload = () => {
console.log(myFile);
Axios.post("http://localhost:10000/uploads", { myFile: myFile });
};
<div>
<h1>GeeksforGeeks</h1>
<h3>File Upload using React!</h3>
<div>
<input
type="file"
onChange={(event) => {
setmyFile(event.target.files[0]);
}}
/>
<button onClick={onFileUpload}>Upload!</button>
</div>
this is server
app.post("/uploads", async (req, res, next) => {
try {
const myFile = req.body.myFile;
console.log(myFile);
const imageUrl = await uploadImage(myFile);
res.status(200).json({
message: "Upload was successful",
data: imageUrl,
});
} catch (error) {
next(error);
}
});
can someone help me , why "myFile" return "{}"
Simple way of file uploading with react to node sever is.
On React here is how you want to handle things using axios
const data = new FormData();
data.append('media_file', file_input) // Note the file in quotes is the key that the server will use to retrive the input i.e **file_input** in this case
axios.post(url, data).then(res=>{
console.log(res)
}).catch(error=>{
console.log(error)
})
So now how you handle this on your nodejs is like this I will be using formidable as bodyParser very easy to use
const Formidable = require("formidable"); //Meant for body parsing
router.post('/api/file-upload', (req, res)=>{
const form = new Formidable.InconmingForm();
form.parse(req, (error, fields, files)=>{
const {media_file} = files
//Destructing 'media_file' remember name that we stated on the client
// it was 'media_file' now that is what I want to de-structure within files which comes
//with formidable
})
})
So now if you log media_file you will see all that you need about file then you can continue with your logic of uploading to google cloud
In client side you have to add your file to formData object, and set the Content-Type header to multipart/form-data.
Client side code -
const onFileUpload = () => {
console.log(myFile);
try {
const formData = new FormData()
formData.append('file', myFile)
Axios.post("http://localhost:10000/uploads", formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
} catch (error) {
console.error('Error while uploading image to server', error)
}
};
<div>
<h1> GeeksforGeeks </h1> <h3 > File Upload using React! </h3>
<div>
<input
type="file"
onChange={
(event) => {
setmyFile(event.target.files[0]);
}
}
/>
<button onClick={onFileUpload}> Upload! </button>
</div>
</div>
Server side:
You have to use multer or some other npm package to upload the files in the server side.
Once image is uploaded to google cloud storage, delete the file from local disk. finally block in below code is deleting the file from local disk once image is uploaded successfully or if there is any error in uploading the image.
const multer = require('multer')
const fs = require('fs')
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'public')
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname)
}
})
const upload = multer({ storage: storage }).single('file')
app.post("/uploads", async (req, res, next) => {
upload(req, res, async function (error) {
if (error instanceof multer.MulterError) {
return res.status(500).json(error)
} else if (error) {
return res.status(500).json(error)
}
const { filename } = req.file
var fileStream = fs.createReadStream(req.file.path)
try {
const options = {
filename
}
const imageUrl = await uploadImage(fileStream, options)
res.status(200).json({
message: "Upload was successful",
data: imageUrl,
});
} catch (error) {
next(error);
} finally {
fs.unlink(req.file.path, function (error) {
if (error) {
console.log('Error on deleting file from the path: ', req.file.path)
}
console.log('File deleted successfully from the disk')
})
}
})
});
As #Akanksha singh mentioned, you need to set the Content-Type header to multipart/form-data and use the formData object on the client side:
const onFileUpload = () => {
console.log(myFile);
try {
const formData = new FormData()
formData.append('file', myFile)
Axios.post("http://localhost:10000/uploads", formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
} catch (error) {
console.error('Error while uploading image to server', error)
}
};
<div>
<h1> GeeksforGeeks </h1> <h3 > File Upload using React! </h3>
<div>
<input
type="file"
onChange={
(event) => {
setmyFile(event.target.files[0]);
}
}
/>
<button onClick={onFileUpload}> Upload! </button>
</div>
</div>
After testing this, logging the req.body on the server side returns a buffer instead of an empty object.
In order to deal with this buffer, which contains the form data, I suggest you follow the steps mentioned in the official docs to deal with multipart/form-data in Cloud Functions
Here is the code sample from the docs:
/**
* Parses a 'multipart/form-data' upload request
*
* #param {Object} req Cloud Function request context.
* #param {Object} res Cloud Function response context.
*/
const path = require('path');
const os = require('os');
const fs = require('fs');
// Node.js doesn't have a built-in multipart/form-data parsing library.
// Instead, we can use the 'busboy' library from NPM to parse these requests.
const Busboy = require('busboy');
exports.uploadFile = (req, res) => {
if (req.method !== 'POST') {
// Return a "method not allowed" error
return res.status(405).end();
}
const busboy = new Busboy({headers: req.headers});
const tmpdir = os.tmpdir();
// This object will accumulate all the fields, keyed by their name
const fields = {};
// This object will accumulate all the uploaded files, keyed by their name.
const uploads = {};
// This code will process each non-file field in the form.
busboy.on('field', (fieldname, val) => {
/**
* TODO(developer): Process submitted field values here
*/
console.log(`Processed field ${fieldname}: ${val}.`);
fields[fieldname] = val;
});
const fileWrites = [];
// This code will process each file uploaded.
busboy.on('file', (fieldname, file, filename) => {
// Note: os.tmpdir() points to an in-memory file system on GCF
// Thus, any files in it must fit in the instance's memory.
console.log(`Processed file ${filename}`);
const filepath = path.join(tmpdir, filename);
uploads[fieldname] = filepath;
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
// File was processed by Busboy; wait for it to be written.
// Note: GCF may not persist saved files across invocations.
// Persistent files must be kept in other locations
// (such as Cloud Storage buckets).
const promise = new Promise((resolve, reject) => {
file.on('end', () => {
writeStream.end();
});
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
fileWrites.push(promise);
});
// Triggered once all uploaded files are processed by Busboy.
// We still need to wait for the disk writes (saves) to complete.
busboy.on('finish', async () => {
await Promise.all(fileWrites);
/**
* TODO(developer): Process saved files here
*/
for (const file in uploads) {
fs.unlinkSync(uploads[file]);
}
res.send();
});
busboy.end(req.rawBody);
};
here is my multer constant declared and the route used for uploading images
let storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, `server/uploads/events/${req.params.group}/${req.params.name}`);
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}_${file.originalname}`);
},
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname)
if (ext !== '.jpg' && ext !== '.png' && ext !== '.mp4') {
return cb(res.status(400).end('only jpg, png, mp4 is allowed'), false);
}
cb(null, true)
}
});
const upload = multer({ storage: storage }).single("file");
router.post("/uploadfiles/:group/:name", (req, res) => {
upload(req, res, async (err) => {
console.log(req.file);
await fs.closeSync(fs.openSync(res.req.file.path + '/' + res.req.filename, 'w'))
if (err) {
console.log(err)
return res.json({ success: false, err });
}else{
console.log(req.file)
return res.json({ success: true, url: res.req.file.path, fileName: res.req.file.filename });
}
});
});
and here is my frontend file where the axios request is defined
onImageChange= e=>{
const image = e.target.files[0]
const reader = new FileReader();
reader.onload = () =>
{
this.setState({
imagesrc:reader.result,
selectedImage:image,
filename:image.name
})
}
reader.readAsDataURL(e.target.files[0]);
const formData = new FormData()
const file = e.target.files[0]
formData.append('file',file)
for (var key of formData.entries()) {
console.log(key[0] + ', ' + key[1]);
}
axios({
method: 'post',
url: `http://localhost:5000/api/groups/events/uploadfiles/${this.props.auth.group.id}/${this.state.name}`,
data: formData
}).then(response=>{
if (response) {
console.log(this.state.selectedImage);
}
})
}
here i have defined selectedImage as an empty array in the initial state.
this is inside my return component where i have called the form
<form noValidate onSubmit={this.onSubmit} autoComplete="off" className="eventspopupinner" encType="multipart/form-data">
<div className="imagecontainer">
<input accept="image/*" className='input' id="icon-button-file" type="file" name="file" onChange={this.onImageChange} style={{display:'none'}}/>
the upload folder needed to exist to allow multer to save an image there
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()
}
I´m using formidable to handle my file uploads in NodeJs. I´m a little stuck at parsing field values.
How do I get the value of project_id to the form handler, so I can write the parameter in my filename?
<input type="text" id="project_id" value="{{projects._id}}" readonly>
EDIT
To be more specific, here´s a detailed view of my form-upload handling:
app.post('/uploads/', function (req, res){
var form = new formidable.IncomingForm();
form.parse(req, function (err, fields, files) {
res.writeHead(200, {'content-type': 'image/jpeg'});
res.write('received upload: \n\n');
var project = fields.project_id;
res.end(util.inspect(project, {fields: fields, files: files}));
});
form.on('end', function(project, fields, files){
console.log(project);
/*Temporary location of our uploaded file */
var temp_path = this.openedFiles[0].path;
/*The file name of the uploaded file */
var file_name = project + '.' + this.openedFiles[0].name;
I can log the var project in the form.parse part. But I don´t get the variable in the form.on('end'... part.
HTML form
<form id="uploadForm"
enctype="multipart/form-data"
action="/uploads/"
method="post">
<input type="text" name="project_id" id="project_id" value="{{projects._id}}" readonly>
<input multiple="multiple" type="file" name="upload" />
<button type="submit">Upload</button>
</form>
Formidable's end callback doesn't take any parameters, but I'm not sure you even need to call it if you're using the parse callback. I think what you're looking for is something like this:
var fs = require('fs');
app.post('/uploads', function(req, res, next) {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
if (err) next(err);
// TODO: make sure my_file and project_id exist
fs.rename(files.my_file.path, fields.project_id, function(err) {
if (err) next(err);
res.end();
});
});
});
You would need to listen for the end() event if you chose not to use the parse callback, like this:
new formidable.IncomingForm().parse(req)
.on('file', function(name, file) {
console.log('Got file:', name);
})
.on('field', function(name, field) {
console.log('Got a field:', name);
})
.on('error', function(err) {
next(err);
})
.on('end', function() {
res.end();
});
Client side script:
//Upload the file
var fd = new FormData();
//Take the first selected file
fd.append("dbDocPath", 'invoices/' + file.name);
fd.append("file", file);
$http({
method: 'POST',
url: $rootScope.apiUrl + 'uploadDocToServer',
data: fd,
headers: {
'Content-Type': undefined
},
//prevents serializing payload. don't do it.
transformRequest: angular.identity,
}).success(function (response) {
if (response.success) {
}
})
Server side script:
var fileDir = path.join(__dirname, '/../uploads');
// create an incoming form object
var form = new formidable.IncomingForm();
var dbDocPath = '';
form.parse(req)
.on('field', function (name, field) {
//console.log('Got a field:', field);
//console.log('Got a field name:', name);
dbDocPath = field;
})
.on('file', function (name, file) {
//console.log('Got file:', name);
// specify that we want to allow the user to upload multiple files in a single request
//form.multiples = true;
// store all uploads in the /uploads directory
form.uploadDir = fileDir;
fs.rename(file.path, path.join(form.uploadDir, file.name));
// every time a file has been uploaded successfully,
// rename it to it's orignal name
var bucket = new AWS.S3();
//console.log(dbDocPath);
var params = {
Bucket: DocsConfig.bucketName,
Key: dbDocPath,
Body: fs.createReadStream(path.join(form.uploadDir, file.name)),
ACL: 'public-read'
};
bucket.putObject(params, function (perr, pres) {
if (perr) {
//console.log("Error uploading data: ", perr);
} else {
fs.unlinkSync(path.join(form.uploadDir, file.name));
//console.log("Successfully uploaded data", pres);
}
});
})
.on('error', function (err) {
res.send({'success': false, error: err});
})
.on('end', function () {
res.send({'success': true});
});
// parse the incoming request containing the form data
//form.parse(req);
Just keep one thing in mind that the sequence of sending parameters to formData() should be same as mentioned in above code as file upload needs path to upload to the destiny.