Build File Upload/Download Functionality with Image Preview

And also learn how to add drag and drop feature to upload any type of file

Yogesh Chavan
Oct 9 · 19 min read
Image for post
Image for post
File Upload

Introduction

In this article, we will create a file upload and download functionality with a preview of the image using the MERN stack.

  • How to upload and download any type of file
  • How to restrict the type of the file while uploading
  • How to restrict the size of the file while uploading
  • How to show a preview of the image after selecting it
  • How to use MongoDB to store and get the details of the file

Initial Setup

Create a new project using create-react-app

create-react-app react-upload-download-files
yarn add axios@0.20.0 bootstrap@4.5.2 downloadjs@1.4.7 node-sass@4.14.1 react-bootstrap@1.3.0 react-dropzone@11.2.0 react-router-dom@5.2.0

Creating Initial Pages

Create a new file with the name Header.js inside the components folder with the following content:

import React from 'react';
import { NavLink } from 'react-router-dom';
const Header = () => {
return (
<div className="header">
<h1>File Upload And Download</h1>
<nav>
<NavLink activeClassName="active" to="/" exact={true}>
Home
</NavLink>
<NavLink activeClassName="active" to="/list">
Files List
</NavLink>
</nav>
</div>
);
};
export default Header;
import React, { useState, useRef } from 'react';
import { Form, Row, Col, Button } from 'react-bootstrap';
const App = (props) => {
const [file, setFile] = useState(null); // state for storing actual image
const [previewSrc, setPreviewSrc] = useState(''); // state for storing previewImage
const [state, setState] = useState({
title: '',
description: ''
});
const [errorMsg, setErrorMsg] = useState('');
const [isPreviewAvailable, setIsPreviewAvailable] = useState(false); // state to show preview only for images
const dropRef = useRef(); // React ref for managing the hover state of droppable area
const handleInputChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
};
const handleOnSubmit = async (event) => {
event.preventDefault();
};
return (
<React.Fragment>
<Form className="search-form" onSubmit={handleOnSubmit}>
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<Row>
<Col>
<Form.Group controlId="title">
<Form.Control
type="text"
name="title"
value={state.title || ''}
placeholder="Enter title"
onChange={handleInputChange}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col>
<Form.Group controlId="description">
<Form.Control
type="text"
name="description"
value={state.description || ''}
placeholder="Enter description"
onChange={handleInputChange}
/>
</Form.Group>
</Col>
</Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</React.Fragment>
);
};
export default App;
const handleInputChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
};
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import App from '../components/App';
import Header from '../components/Header';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<div className="main-content">
<Switch>
<Route component={App} path="/" exact={true} />
</Switch>
</div>
</div>
</BrowserRouter>
);
export default AppRouter;
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';
ReactDOM.render(<AppRouter />, document.getElementById('root'));
Image for post
Image for post

Adding File Upload Functionality

Now, let’s add the option to upload the file from the UI.

<div className="upload-section">
<Dropzone onDrop={onDrop}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps({ className: 'drop-zone' })} ref={dropRef}>
<input {...getInputProps()} />
<p>Drag and drop a file OR click here to select a file</p>
{file && (
<div>
<strong>Selected file:</strong> {file.name}
</div>
)}
</div>
)}
</Dropzone>
{previewSrc ? (
isPreviewAvailable ? (
<div className="image-preview">
<img className="preview-image" src={previewSrc} alt="Preview" />
</div>
) : (
<div className="preview-message">
<p>No preview available for this file</p>
</div>
)
) : (
<div className="preview-message">
<p>Image preview will be shown here after selection</p>
</div>
)}
</div>
import Dropzone from 'react-dropzone';
import axios from 'axios';
const onDrop = (files) => {
const [uploadedFile] = files;
setFile(uploadedFile);
const fileReader = new FileReader();
fileReader.onload = () => {
setPreviewSrc(fileReader.result);
};
fileReader.readAsDataURL(uploadedFile);
setIsPreviewAvailable(uploadedFile.name.match(/\.(jpeg|jpg|png)$/));
};
const [uploadedFile] = files;
fileReader.onload = () => {
setPreviewSrc(fileReader.result);
};
setIsPreviewAvailable(uploadedFile.name.match(/\.(jpeg|jpg|png)$/));
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

Add drop indication

If you saw the drop functionality, we’re not showing any indication that the file is being dropped into the drop area so let’s add that.

<div {...getRootProps({ className: 'drop-zone' })} ref={dropRef}>
<Dropzone
onDrop={onDrop}
onDragEnter={() => updateBorder('over')}
onDragLeave={() => updateBorder('leave')}
>
const updateBorder = (dragState) => {
if (dragState === 'over') {
dropRef.current.style.border = '2px solid #000';
} else if (dragState === 'leave') {
dropRef.current.style.border = '2px dashed #e9ebeb';
}
};
dropRef.current.style.border = '2px dashed #e9ebeb';
Image for post
Image for post

Calling API For File Upload

Create a new file with the name constants.js inside the src/utils folder with the following content

export const API_URL = 'http://localhost:3030';
const handleOnSubmit = async (event) => {
event.preventDefault();
try {
const { title, description } = state;
if (title.trim() !== '' && description.trim() !== '') {
if (file) {
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
formData.append('description', description);
setErrorMsg('');
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
} else {
setErrorMsg('Please select a file to add.');
}
} else {
setErrorMsg('Please enter all the field values.');
}
} catch (error) {
error.response && setErrorMsg(error.response.data);
}
};
import { API_URL } from '../utils/constants';
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});

Adding server-side code for file upload

Now, let’s add the server-side functionality to upload the file.

yarn init -y
yarn add cors@2.8.5 express@4.17.1 mongoose@5.10.7 multer@1.4.2 nodemon@2.0.4
node_modules
const mongoose = require('mongoose');mongoose.connect('mongodb://127.0.0.1:27017/file_upload', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
const mongoose = require('mongoose');const fileSchema = mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
file_path: {
type: String,
required: true
},
file_mimetype: {
type: String,
required: true
}
},
{
timestamps: true
}
);
const File = mongoose.model('File', fileSchema);module.exports = File;
const path = require('path');
const express = require('express');
const multer = require('multer');
const File = require('../model/file');
const Router = express.Router();
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, './files');
},
filename(req, file, cb) {
cb(null, `${new Date().getTime()}_${file.originalname}`);
}
}),
limits: {
fileSize: 1000000 // max file size 1MB = 1000000 bytes
},
fileFilter(req, file, cb) {
if (!file.originalname.match(/\.(jpeg|jpg|png|pdf|doc|docx|xlsx|xls)$/)) {
return cb(
new Error(
'only upload files with jpg, jpeg, png, pdf, doc, docx, xslx, xls format.'
)
);
}
cb(undefined, true); // continue with upload
}
});
Router.post(
'/upload',
upload.single('file'),
async (req, res) => {
try {
const { title, description } = req.body;
const { path, mimetype } = req.file;
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
await file.save();
res.send('file uploaded successfully.');
} catch (error) {
res.status(400).send('Error while uploading file. Try again later.');
}
},
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
}
);
Router.get('/getAllFiles', async (req, res) => {
try {
const files = await File.find({});
const sortedByCreationDate = files.sort(
(a, b) => b.createdAt - a.createdAt
);
res.send(sortedByCreationDate);
} catch (error) {
res.status(400).send('Error while getting list of files. Try again later.');
}
});
Router.get('/download/:id', async (req, res) => {
try {
const file = await File.findById(req.params.id);
res.set({
'Content-Type': file.file_mimetype
});
res.sendFile(path.join(__dirname, '..', file.file_path));
} catch (error) {
res.status(400).send('Error while downloading file. Try again later.');
}
});
module.exports = Router;
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, './files');
},
filename(req, file, cb) {
cb(null, `${new Date().getTime()}_${file.originalname}`);
}
}),
limits: {
fileSize: 1000000 // max file size 1MB = 1000000 bytes
},
fileFilter(req, file, cb) {
if (!file.originalname.match(/\.(jpeg|jpg|png|pdf|doc|docx|xlsx|xls)$/)) {
return cb(
new Error(
'only upload files with jpg, jpeg, png, pdf, doc, docx, xslx, xls format.'
)
);
}
cb(undefined, true); // continue with upload
}
});
destination(req, file, cb) {
destination: function(req, file, cb) {
  • If there is no error, then the first argument will be null or undefined and the second argument will contain the data be passed to the callback function.
Router.post(
'/upload',
upload.single('file'),
async (req, res) => {
try {
const { title, description } = req.body;
const { path, mimetype } = req.file;
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
await file.save();
res.send('file uploaded successfully.');
} catch (error) {
res.status(400).send('Error while uploading file. Try again later.');
}
},
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
}
);
const formData = new FormData();
formData.append('file', file);
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
};
const express = require('express');
const cors = require('cors');
const fileRoute = require('./routes/file');
require('./db/db');
const app = express();app.use(cors());
app.use(fileRoute);
app.listen(3030, () => {
console.log('server started on port 3030');
});
"scripts": {
"start": "nodemon index.js"
}
yarn start
./mongod --dbpath=<path_to_mongodb-data_folder>
Image for post
Image for post
import React, { useState, useEffect } from 'react';
import download from 'downloadjs';
import axios from 'axios';
import { API_URL } from '../utils/constants';
const FilesList = () => {
const [filesList, setFilesList] = useState([]);
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
const getFilesList = async () => {
try {
const { data } = await axios.get(`${API_URL}/getAllFiles`);
setErrorMsg('');
setFilesList(data);
} catch (error) {
error.response && setErrorMsg(error.response.data);
}
};
getFilesList();
}, []);
const downloadFile = async (id, path, mimetype) => {
try {
const result = await axios.get(`${API_URL}/download/${id}`, {
responseType: 'blob'
});
const split = path.split('/');
const filename = split[split.length - 1];
setErrorMsg('');
return download(result.data, filename, mimetype);
} catch (error) {
if (error.response && error.response.status === 400) {
setErrorMsg('Error while downloading file. Try again later');
}
}
};
return (
<div className="files-container">
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<table className="files-table">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Download File</th>
</tr>
</thead>
<tbody>
{filesList.length > 0 ? (
filesList.map(
({ _id, title, description, file_path, file_mimetype }) => (
<tr key={_id}>
<td className="file-title">{title}</td>
<td className="file-description">{description}</td>
<td>
<a
href="#/"
onClick={() =>
downloadFile(_id, file_path, file_mimetype)
}
>
Download
</a>
</td>
</tr>
)
)
) : (
<tr>
<td colSpan={3} style={{ fontWeight: '300' }}>
No files found. Please add some.
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default FilesList;
Router.get('/getAllFiles', async (req, res) => {
try {
const files = await File.find({});
const sortedByCreationDate = files.sort(
(a, b) => b.createdAt - a.createdAt
);
res.send(sortedByCreationDate);
} catch (error) {
res.status(400).send('Error while getting list of files. Try again later.');
}
});
const { data } = await axios.get(`${API_URL}/getAllFiles`);
setErrorMsg('');
setFilesList(data);
const downloadFile = async (id, path, mimetype) => {
try {
const result = await axios.get(`${API_URL}/download/${id}`, {
responseType: 'blob'
});
const split = path.split('/');
const filename = split[split.length - 1];
setErrorMsg('');
return download(result.data, filename, mimetype);
} catch (error) {
if (error.response && error.response.status === 400) {
setErrorMsg('Error while downloading file. Try again later');
}
}
};
Router.get('/download/:id', async (req, res) => {
try {
const file = await File.findById(req.params.id);
res.set({
'Content-Type': file.file_mimetype
});
res.sendFile(path.join(__dirname, '..', file.file_path));
} catch (error) {
res.status(400).send('Error while downloading file. Try again later.');
}
});
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import App from '../components/App';
import Header from '../components/Header';
import FilesList from '../components/FilesList';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<div className="main-content">
<Switch>
<Route component={App} path="/" exact={true} />
<Route component={FilesList} path="/list" />
</Switch>
</div>
</div>
</BrowserRouter>
);
export default AppRouter;
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
props.history.push('/list'); // add this line

Uploading Image File Demo

Image for post
Image for post

Uploading PDF File Demo

Image for post
Image for post

Uploading Excel File Demo

Image for post
Image for post

Uploading Doc file Demo

Image for post
Image for post

Uploading un-supported File Demo

Image for post
Image for post

Removing the Need of CORS

As previously mentioned, to stop getting CORS error while calling API from React App to Node.js App, we’re using cors library at the server-side like this:

app.use(cors());
Image for post
Image for post
app.use(express.static(path.join(__dirname, '..', 'build')));
const path = require('path');
const express = require('express');
const path = require('path');
const fileRoute = require('./routes/file');
require('./db/db');
const app = express();app.use(express.static(path.join(__dirname, '..', 'build')));
app.use(fileRoute);
app.listen(3030, () => {
console.log('server started on port 3030');
});
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-app": "yarn build && (cd server && yarn start)"
},

Conclusion

We have now finished creating the complete file upload and download functionality using MERN stack.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store