14 | Looks Like You've Landed on The Wrong Page!
15 |
16 |
17 | Click
18 |
19 | {" "}
20 | Here{" "}
21 |
22 | to Go to The Home Page.
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default NotFound;
30 |
--------------------------------------------------------------------------------
/server-side/models/file.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | // The schema for the file model
6 | const fileSchema = new Schema(
7 | {
8 | file_key: { type: String, required: true, trim: true }, // The file key after S3 file upload is done
9 | file_mimetype: { type: String, required: true, trim: true }, // The mimetype helps download the correct filetype later
10 | file_location: { type: String, required: true, trim: true }, // The URL to download the file, provided after AWS S3 file upload is done
11 | file_name: { type: String, required: true, trim: true }, // The original name of the file that has been uploaded, without the date-time prefix
12 | },
13 | {
14 | timestamps: true,
15 | }
16 | );
17 |
18 | // Set the Time-to-Live to be 15 days, to potentially save space in the mongoDB database
19 | fileSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 * 24 * 15 });
20 | const File = mongoose.model("File", fileSchema);
21 |
22 | module.exports = File;
23 |
--------------------------------------------------------------------------------
/src/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import "../stylesheets/navbar.css";
4 | import logo from "../stylesheets/logo.png";
5 |
6 | // Navbar component
7 | const Navbar = () => {
8 | return (
9 |
29 | );
30 | };
31 |
32 | export default Navbar;
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rajat M
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/stylesheets/notFound.css:
--------------------------------------------------------------------------------
1 | /* basinc style for the 404-error page */
2 | div.error-page {
3 | display: flex;
4 | margin-top:8em;
5 | flex-flow: column nowrap;
6 | justify-content: space-evenly;
7 | align-items: center;
8 | }
9 |
10 | div.error-text {
11 | display: inline-flex;
12 | flex-flow: column nowrap;
13 | justify-content: space-evenly;
14 | align-items: center;
15 | }
16 |
17 | img.err-404 {
18 | width:5em;
19 | height:5em;
20 | }
21 |
22 | p.not-found {
23 | color: #08a1c4;
24 | font-family: "Poppins",sans-serif;
25 | font-size:1.2em;
26 | line-height: 1.2em;
27 | font-weight: 500;
28 | display: inline-flex;
29 | justify-content: center;
30 | align-items:center;
31 | }
32 |
33 | a.link-home {
34 | color:#02b875;
35 | border-bottom: 2px dashed #08a1c4;
36 | font-weight: 700;
37 | margin: 0 0.2em;
38 | }
39 |
40 | /* media query for mobile screens */
41 | @media screen and (max-width:600px) {
42 | div.error-page {
43 | max-width: 90vw;
44 | align-self: center;
45 | }
46 |
47 | img.err-404 {
48 | width: 5em;
49 | height: 5em;
50 | }
51 |
52 | p.not-found {
53 | flex-flow: row wrap;
54 | text-align: center;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from "react";
2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; // React Router
3 |
4 | // import all the required components
5 | import Home from "./components/Home";
6 | import Download from "./components/Download";
7 | import Navbar from "./components/Navbar";
8 | import Footer from "./components/Footer";
9 | import NotFound from "./components/NotFound";
10 | import About from "./components/About";
11 | import "./App.css";
12 |
13 | const App = () => {
14 | return (
15 |
16 |
17 |
18 |
19 | {/* Home page component */}
20 |
21 | {/* The donwload page needs to have the id of the file to be downloaded in its params */}
22 |
23 | {/* The about page */}
24 |
25 | {/* A catch-all page to display 404-error */}
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default App;
35 |
--------------------------------------------------------------------------------
/server-side/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express"); // express framework to build the application
2 | const mongoose = require("mongoose"); // mongoose ODM to handle mongoDB documents
3 | const cors = require("cors"); // package to avoid CORS errors
4 | const path = require("path"); // to handle static files' path to help deploy the app to heroku
5 | require("dotenv").config(); // package to handle the environment vars
6 |
7 | // configure the express app to use JSON and CORS()
8 | const app = express();
9 | app.use(cors());
10 | app.use(express.json());
11 |
12 | // configure the port for the server side
13 | const PORT = process.env.PORT || 5000;
14 |
15 | // configure the mongoose setup
16 | const uri = process.env.MONGO_URI;
17 | mongoose.connect(uri, {
18 | useNewUrlParser: true,
19 | useUnifiedTopology: true,
20 | useCreateIndex: true,
21 | });
22 | const connection = mongoose.connection;
23 | connection.once("open", () => {
24 | console.log("MongoDB connection has been established");
25 | });
26 |
27 | // configure the routes
28 | const fileRouter = require("./routes/file.route");
29 | app.use("/api/file", fileRouter);
30 |
31 | //Load the npm build package of the frontend CRA
32 | if (process.env.NODE_ENV === "production") {
33 | // set a static folder
34 | app.use(express.static("build"));
35 |
36 | // Provide a wildcard as a fallback for all routes
37 | app.get("*", (req, res) => {
38 | res.sendFile(path.resolve(__dirname, "../build", "index.html"));
39 | });
40 | }
41 |
42 | // host the app on port
43 | app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
44 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "../stylesheets/footer.css";
3 |
4 | // component to display footer
5 | const Footer = () => {
6 | return (
7 |
54 | );
55 | };
56 |
57 | export default Footer;
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file-share",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "aws-sdk": "^2.832.0",
10 | "axios": "^0.21.1",
11 | "cors": "^2.8.5",
12 | "dotenv": "^8.2.0",
13 | "downloadjs": "^1.4.7",
14 | "express": "^4.17.1",
15 | "materialize-css": "^1.0.0-rc.2",
16 | "mongoose": "^5.11.11",
17 | "multer": "^1.4.2",
18 | "multer-s3": "^2.9.0",
19 | "react": "^17.0.1",
20 | "react-dom": "^17.0.1",
21 | "react-dropzone": "^11.2.4",
22 | "react-router-dom": "^5.2.0",
23 | "react-scripts": "4.0.1",
24 | "web-vitals": "^0.2.4",
25 | "workbox-background-sync": "^5.1.3",
26 | "workbox-broadcast-update": "^5.1.3",
27 | "workbox-cacheable-response": "^5.1.3",
28 | "workbox-core": "^5.1.3",
29 | "workbox-expiration": "^5.1.3",
30 | "workbox-google-analytics": "^5.1.3",
31 | "workbox-navigation-preload": "^5.1.3",
32 | "workbox-precaching": "^5.1.3",
33 | "workbox-range-requests": "^5.1.3",
34 | "workbox-routing": "^5.1.3",
35 | "workbox-strategies": "^5.1.3",
36 | "workbox-streams": "^5.1.3"
37 | },
38 | "scripts": {
39 | "start": "node server-side/server.js",
40 | "build": "react-scripts build",
41 | "test": "react-scripts test",
42 | "eject": "react-scripts eject",
43 | "client": "yarn start",
44 | "server": "nodemon server-side/server.js",
45 | "heroku-postbuild": "npm install && npm run build",
46 | "dev": "concurrently --kill-others-on-fail \"yarn server\" \"yarn client\""
47 | },
48 | "eslintConfig": {
49 | "extends": [
50 | "react-app",
51 | "react-app/jest"
52 | ]
53 | },
54 | "browserslist": {
55 | "production": [
56 | ">0.2%",
57 | "not dead",
58 | "not op_mini all"
59 | ],
60 | "development": [
61 | "last 1 chrome version",
62 | "last 1 firefox version",
63 | "last 1 safari version"
64 | ]
65 | },
66 | "description": "A MERN stack file-share application with a paymeny gateway",
67 | "main": "server.js",
68 | "devDependencies": {
69 | "concurrently": "^5.3.0"
70 | },
71 | "author": "Rajat M",
72 | "license": "ISC",
73 | "proxy": "http://localhost:5000/"
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Download.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import axios from "axios";
3 | import download from "downloadjs"; // package to trigger file downloads on the clientside
4 | import "../stylesheets/download.css";
5 |
6 | // configure the baseURL to be either the localhost or the deployed URL
7 | const baseURL = process.env.REACT_APP_BASEURL || "http://localhost:5000";
8 | let frontURL = "";
9 | if (baseURL === "http://localhost:5000") {
10 | frontURL = "http://localhost:3000";
11 | }
12 |
13 | // component to handle file downloads after user clicks on the shareable link
14 | const Download = (props) => {
15 | // trigger the file download after the component has mounted
16 | useEffect(() => {
17 | // fetch the _id of the File(from DB) from the params
18 | const id = props.match.params.id;
19 |
20 | // Get the data for the correct file object using the id
21 | axios
22 | .get(`${baseURL}/api/file/${id}`)
23 | .then((file) => {
24 | const downloadFile = file.data; // the buffer array that holds the content of the file
25 |
26 | // invoke the download function to download the file
27 | download(
28 | Uint8Array.from(downloadFile.data.Body.data).buffer, // convert the buffer array to uint8array, to be compliant with the downloadjs function property type
29 | downloadFile.file.file_name, // name of the file to be downloaded, is set to the original file name stored in the DB
30 | downloadFile.file.file_mimetype // the valid mime type of the file to be downloaded
31 | );
32 | })
33 | .then(() =>
34 | // wait for a second to finish file download, and then redirect to the home page of the application
35 | window.setTimeout(() => {
36 | window.location.replace(frontURL || baseURL);
37 | }, 1000)
38 | )
39 | .catch((err) => {
40 | if (err.message) {
41 | alert("No such file is available in the server!");
42 | window.location.replace(baseURL || frontURL);
43 | } else {
44 | console.log(JSON.stringify(err));
45 | }
46 | });
47 | }, []);
48 | return
69 |
70 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/stylesheets/about.css:
--------------------------------------------------------------------------------
1 | /* stylesheet for the about page */
2 |
3 | /* the main section of the page */
4 | div.about {
5 | margin-top:5em;
6 | display: inline-flex;
7 | flex-flow: column wrap;
8 | justify-content: space-around;
9 | align-items: flex-start;
10 | max-width: 75vw;
11 | min-height: 27em;
12 | margin-left: 10vw;
13 | }
14 |
15 | /* the main heading of the about section */
16 | .about h1.about-heading {
17 | font-family: "Poppins", sans-serif;
18 | font-size: 2.7em;
19 | line-height: 0.2em;
20 | font-weight: 500;
21 | color: #08cfbe;
22 | }
23 |
24 | /* the paragraphs in the about section */
25 | .about section.main-text {
26 | display: inline-flex;
27 | flex-flow: row nowrap;
28 | justify-content: center;
29 | align-items: center;
30 | width:100%;
31 | margin-top: -5em;
32 | }
33 |
34 | .about-text p.about-p {
35 | font-family: "Poppins", sans-serif;
36 | font-size: 1em;
37 | line-height: 1.5em;
38 | font-weight: 400;
39 | color: #08a1c4;
40 | width:80%;
41 | }
42 |
43 | /* to style the font icon present in the desktop view of the about section */
44 | i.lni-question-circle {
45 | font-size: 12em;
46 | color: #08cfbe;
47 | align-self: flex-end;
48 | margin-bottom: 0.3em;
49 | }
50 |
51 | /* giving a different font color to highlight the supported file types */
52 | span.file-type {
53 | color: #02b875;
54 | font-weight: 500;
55 | text-decoration: underline;
56 | }
57 |
58 | /* style the horizontal rule present below the heading */
59 | hr.about-hr {
60 | border: 0;
61 | height: 1px;
62 | background-image: linear-gradient(to left, rgba(0, 0, 0, 0), #08cfbe, rgba(0, 0, 0, 0));
63 | }
64 |
65 | /* media queries for the mobile screens */
66 | @media screen and (max-width: 600px) {
67 |
68 | /* change dimensions of about section's display */
69 | div.about {
70 | margin-top: 4em;
71 | flex-flow: column nowrap;
72 | max-width: 80vw;
73 | margin-left: 5vw;
74 | }
75 |
76 | /* reduce font size and increase width to cover most of the page */
77 | .about h1.about-heading {
78 | font-size: 2.1em;
79 | line-height: 1.2em;
80 | margin: 1em 0;
81 | width:70%;
82 | }
83 |
84 | hr.about-hr {
85 | border: 0;
86 | height: 2px;
87 | align-self: flex-start;
88 | background: #08cfbe;
89 | width: 50%;
90 | margin: 0;
91 | }
92 |
93 | .about section.main-text {
94 | width: 110%;
95 | margin-top: -2em;
96 | }
97 |
98 | .about-text p.about-p {
99 | width: 100%;
100 | }
101 |
102 | /* do not display the icon on mobile screens */
103 | i.lni-question-circle {
104 | display:none;
105 | visibility: hidden;
106 | opacity: 0;
107 | }
108 | }
--------------------------------------------------------------------------------
/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "../stylesheets/about.css";
3 |
4 | // component to act as the About page
5 | const About = () => {
6 | return (
7 |
8 |
9 | About EasyShare.
10 |
11 |
12 |
13 |
14 |
15 | This is an application to help share files with ease.
16 |
17 |
18 |
19 | Just drag and drop any file or choose any file from your
20 | system. Once the file is uploaded, you can either
21 | download the file from that page or you can also get a
22 | link to share the file.
23 |
24 |
25 | The supported file formats are:{" "}
26 | png,{" "}
27 | jpg,{" "}
28 | jpeg,{" "}
29 | webp,{" "}
30 | svg,{" "}
31 | gif,{" "}
32 | doc,{" "}
33 | docx,{" "}
34 | pdf,{" "}
35 | ppt,{" "}
36 | pptx,{" "}
37 | xls,{" "}
38 | xlsx. The shareable
39 | link is valid for 15 days, after which you will have to
40 | re-upload the required file.
41 |
42 |
43 | In case of any issues with the app, contact the
44 | developer using the links in the footer or by dropping a
45 | mail to{" "}
46 |
50 | this email-id
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default About;
61 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from 'workbox-core';
11 | import { ExpirationPlugin } from 'workbox-expiration';
12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
13 | import { registerRoute } from 'workbox-routing';
14 | import { StaleWhileRevalidate } from 'workbox-strategies';
15 |
16 | clientsClaim();
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST);
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== 'navigate') {
33 | return false;
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith('/_')) {
37 | return false;
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | if (url.pathname.match(fileExtensionRegexp)) {
41 | return false;
42 | } // Return true to signal that we want to use the handler.
43 |
44 | return true;
45 | },
46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
47 | );
48 |
49 | // An example runtime caching route for requests that aren't handled by the
50 | // precache, in this case same-origin .png requests like those from in public/
51 | registerRoute(
52 | // Add in any other file extensions or routing criteria as needed.
53 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
54 | new StaleWhileRevalidate({
55 | cacheName: 'images',
56 | plugins: [
57 | // Ensure that once this runtime cache reaches a maximum size the
58 | // least-recently used images are removed.
59 | new ExpirationPlugin({ maxEntries: 50 }),
60 | ],
61 | })
62 | );
63 |
64 | // This allows the web app to trigger skipWaiting via
65 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
66 | self.addEventListener('message', (event) => {
67 | if (event.data && event.data.type === 'SKIP_WAITING') {
68 | self.skipWaiting();
69 | }
70 | });
71 |
72 | // Any other custom service worker logic can go here.
73 |
--------------------------------------------------------------------------------
/src/serviceWorkerRegistration.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://cra.link/PWA'
45 | );
46 | });
47 | } else {
48 | // Is not localhost. Just register service worker
49 | registerValidSW(swUrl, config);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then((registration) => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | if (installingWorker == null) {
62 | return;
63 | }
64 | installingWorker.onstatechange = () => {
65 | if (installingWorker.state === 'installed') {
66 | if (navigator.serviceWorker.controller) {
67 | // At this point, the updated precached content has been fetched,
68 | // but the previous service worker will still serve the older
69 | // content until all client tabs are closed.
70 | console.log(
71 | 'New content is available and will be used when all ' +
72 | 'tabs for this page are closed. See https://cra.link/PWA.'
73 | );
74 |
75 | // Execute callback
76 | if (config && config.onUpdate) {
77 | config.onUpdate(registration);
78 | }
79 | } else {
80 | // At this point, everything has been precached.
81 | // It's the perfect time to display a
82 | // "Content is cached for offline use." message.
83 | console.log('Content is cached for offline use.');
84 |
85 | // Execute callback
86 | if (config && config.onSuccess) {
87 | config.onSuccess(registration);
88 | }
89 | }
90 | }
91 | };
92 | };
93 | })
94 | .catch((error) => {
95 | console.error('Error during service worker registration:', error);
96 | });
97 | }
98 |
99 | function checkValidServiceWorker(swUrl, config) {
100 | // Check if the service worker can be found. If it can't reload the page.
101 | fetch(swUrl, {
102 | headers: { 'Service-Worker': 'script' },
103 | })
104 | .then((response) => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then((registration) => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log('No internet connection found. App is running in offline mode.');
124 | });
125 | }
126 |
127 | export function unregister() {
128 | if ('serviceWorker' in navigator) {
129 | navigator.serviceWorker.ready
130 | .then((registration) => {
131 | registration.unregister();
132 | })
133 | .catch((error) => {
134 | console.error(error.message);
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/server-side/routes/file.route.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router(); // to handle express routes
2 | const multer = require("multer"); // to handle file uploads in Node.js
3 | const aws = require("aws-sdk"); // to connect to the S3 bucket, we use the aws-sdk
4 | const multerS3 = require("multer-s3"); // to help deal with file upload to the S3 bucket
5 | let File = require("../models/file.model"); // The file model to create new models
6 |
7 | // Create a new instance of the S3 bucket object with the correct user credentials
8 | const s3 = new aws.S3({
9 | accessKeyId: process.env.S3_ACCESS_KEY_ID,
10 | secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
11 | Bucket: "easysharebucket",
12 | });
13 |
14 | // Setup the congifuration needed to use multer
15 | const upload = multer({
16 | // Use the following block of code to store the files in local storage on file upload
17 |
18 | // storage: multer.diskStorage({
19 | // destination(req, file, cb) {
20 | // cb(null, "./server-side/sent_files");
21 | // },
22 | // filename(req, file, cb) {
23 | // cb(null, `${new Date().getTime()}__${file.originalname}`);
24 | // },
25 | // }),
26 |
27 | // Set the storage as the S3 bucker using the correct configuration
28 | storage: multerS3({
29 | s3,
30 | acl: "public-read", // public S3 object, that can be read
31 | bucket: "easysharebucket", // bucket name
32 | key: function (req, file, cb) {
33 | // callback to name the file object in the S3 bucket
34 | // The filename is prefixed with the current time, to avoid multiple files of same name being uploaded to the bucket
35 | cb(null, `${new Date().getTime()}__${file.originalname}`);
36 | },
37 | }),
38 | limits: {
39 | fileSize: 5000000, // maximum file size of 5 MB per file
40 | },
41 |
42 | // Configure the list of file types that are valid
43 | fileFilter(req, file, cb) {
44 | if (
45 | !file.originalname.match(
46 | /\.(jpeg|jpg|png|webp|gif|pdf|doc|docx|xls|xlsx|svg|ppt|pptx)$/
47 | )
48 | ) {
49 | return cb(
50 | new Error(
51 | "Unsupported file format, please choose a different file and retry."
52 | )
53 | );
54 | }
55 | cb(undefined, true); // continue with file upload without errors
56 | },
57 | });
58 |
59 | // // Root Route to get all the files in the reverse chronological order of file upload time
60 | // router.get("/", (req, res) => {
61 | // try {
62 | // File.find()
63 | // .then(
64 | // (files) =>
65 | // res.json(files.sort((a, b) => b.createdAt - a.createdAt)) // sort in reverse chronological order
66 | // )
67 | // .catch((err) => res.status(400).json(`Error: ${err}`));
68 | // } catch (error) {
69 | // if (error) res.status(500).json(error.message);
70 | // }
71 | // });
72 |
73 | // Route to upload new file
74 | router.post(
75 | "/",
76 |
77 | // use the multer configured 'upload' object as middleware, setup a single file upload
78 | upload.single("file"),
79 |
80 | // After the multer module takes care of uploading the file to the S3 bucket, create the corresponding DB document with required fields
81 | (req, res) => {
82 | const { key, mimetype, location } = req.file;
83 | const lastUnderScore = key.lastIndexOf("__");
84 | const file_name = key.slice(lastUnderScore + 2);
85 | const file = new File({
86 | file_key: key,
87 | file_mimetype: mimetype,
88 | file_location: location,
89 | file_name,
90 | });
91 |
92 | // Upon succefully saving the file object in the DB, return the created object
93 | file.save()
94 | .then(() => {
95 | File.findOne({ file_key: key })
96 | .then((file) => {
97 | res.json(file);
98 | })
99 | .catch((err) => res.status(400).send(`Error: ${err}`));
100 | })
101 | .catch((err) => res.status(400).json(`Error: ${err}`));
102 | },
103 | (error, req, res, next) => {
104 | if (error) {
105 | res.status(500).send(error.message);
106 | }
107 | }
108 | );
109 |
110 | // Route to get a particular file object from the DB
111 | router.get("/:id", (req, res) => {
112 | File.findById(req.params.id)
113 | .then((file) => {
114 | // Set the response content type to be the file's mimetype to avoid issues with blob type response
115 | res.set({
116 | "Content-Type": file.file_mimetype,
117 | });
118 |
119 | // The params are required to access objects from the S3 bucket
120 | const params = {
121 | Key: file.file_key,
122 | Bucket: "easysharebucket",
123 | };
124 |
125 | // get the correct S3 object using the File object fetched from the DB
126 | s3.getObject(params, (err, data) => {
127 | if (err) {
128 | res.status(400).json(`Error: ${err}`);
129 | } else {
130 | // return the file object from the DB, as well as the actual file data in the form of a buffer array from the S3 object
131 | res.json({ file, data });
132 | }
133 | });
134 | })
135 | .catch((err) => res.status(400).json(`Error: ${err}`));
136 | });
137 |
138 | // route to delete all the files from the DB, not from the S3 bucket
139 | router.delete("/", (req, res) => {
140 | File.deleteMany({}).then(() => res.json("All files deleted"));
141 | });
142 |
143 | // return the router with all the configured routes
144 | module.exports = router;
145 |
--------------------------------------------------------------------------------
/src/stylesheets/home.css:
--------------------------------------------------------------------------------
1 | /* style for the components on the home page */
2 |
3 | /* remove outlines for buttons */
4 | button:focus {
5 | outline: transparent;
6 | }
7 |
8 | /* set flexbox display */
9 | section.home {
10 | display: flex;
11 | flex-flow: column nowrap;
12 | margin-top: 4em;
13 | align-items: center;
14 | font-family: 'Poppins', sans-serif;
15 | opacity: 0;
16 | transition: opacity 0.3s ease-in-out;
17 | }
18 |
19 | /* style the error message that has to be displayed in case of file upload errors */
20 | section.home p.error-msg {
21 | font-size: 1em;
22 | line-height: 1.5em;
23 | font-weight: 500;
24 | color: #08a1c4;
25 | transition: 0.3s all ease-in-out;
26 | }
27 |
28 | /* style for the drag-and-drop section, along with progress bar and all the links */
29 | section.file-upload {
30 | display:flex;
31 | flex-flow: column nowrap;
32 | min-width: 60vw;
33 | max-width: 70vw;
34 | min-height: 50vh;
35 | align-items: center;
36 | }
37 |
38 | /* style the drag-and-drop section */
39 | section.file-upload div.drop-zone {
40 | min-width:100%;
41 | min-height: 50vh;
42 | font-size:1em;
43 | line-height: 1.5em;
44 | display: flex;
45 | text-align: center;
46 | flex-flow: column nowrap;
47 | justify-content: center;
48 | align-items: center;
49 | border: 2px dashed #08a1c4;
50 | transition: background 0.3s ease-in-out, color 0.3s ease-in-out;
51 | color:#08a1c4;
52 | font-weight: 500;
53 | }
54 |
55 | section.file-upload div.drop-zone:focus {
56 | outline: none;
57 | }
58 |
59 | section.file-upload div.drop-zone:active {
60 | background: #08a1c4;
61 | background: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%);
62 | }
63 |
64 | form.file-form {
65 | display: flex;
66 | flex-flow: column nowrap;
67 | }
68 |
69 |
70 | /* style the submit button */
71 | form.file-form button.submit-btn {
72 | display: inline-flex;
73 | flex-flow: row nowrap;
74 | justify-content: space-evenly;
75 | align-items: center;
76 | align-self: center;
77 | border: 3px solid;
78 | border-image-slice: 1;
79 | background: rgba(8, 207, 78, 0.9);
80 | color: white;
81 | border-image-source: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%);
82 | background: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%);
83 | cursor: pointer;
84 | width:15vw;
85 | height:5vh;
86 | font-weight: 700;
87 | font-size:1.2em;
88 | line-height: 1.2em;
89 | transition: all 0.2s ease-in-out;
90 | }
91 |
92 | form.file-form button.submit-btn:hover {
93 | background: transparent;
94 | color: #08a1c4;
95 | }
96 |
97 | img.submit-icon {
98 | width:1.1em;
99 | height:1.1em;
100 | transition: all 0.3s ease-in-out;
101 | }
102 |
103 | /* use the same white logo used in the navbar and apply various properties to change its color to match the green hue */
104 | button.submit-btn:hover img.submit-icon {
105 | filter: invert(57%) sepia(22%) saturate(7130%) hue-rotate(155deg) brightness(89%) contrast(94%);
106 |
107 | }
108 |
109 | /* style the preview message in the file upload section */
110 | .file-upload .image-preview-message {
111 | display: inline-flex;
112 | align-items: center;
113 | justify-content: center;
114 | width:100%;
115 | height: 50%;
116 | color: #08a1c4;
117 | font-size:1em;
118 | line-height: 1.5em;
119 | font-weight: 500;
120 | }
121 |
122 | .file-upload div.image-preview {
123 | display:inline-flex;
124 | flex-flow: row wrap;
125 | justify-content: center;
126 | }
127 |
128 | /* style for the image that is to be previewed */
129 | div.image-preview>img.preview-image {
130 | max-width: 70%;
131 | height: auto;
132 | max-height: 50vh;
133 | margin: 1em 0;
134 | opacity:0;
135 | /* border: 3px solid; */
136 | /* border-image-slice: 1; */
137 | /* border-width: 2px; */
138 | /* border-image-source: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%); */
139 | transition: 0.3s opacity ease-in-out;
140 | z-index:10;
141 | }
142 |
143 | /* set the max width of the progress bar */
144 | div.progress {
145 | min-width: 50vw;
146 | max-width: 60vw;
147 | }
148 |
149 | /* color of the progress bar is set to be the same gradient as the navbar */
150 | .progress .determinate {
151 | background: linear-gradient(90deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%);
152 | -moz-border-radius: 1em;
153 | -webkit-border-radius: 1em;
154 | border-radius: 0.5em;
155 | }
156 |
157 | /* style the share button to display the tooltip in the required position */
158 | button.share-link {
159 | position: relative;
160 | display: inline-block;
161 | }
162 |
163 | /* set the style to the tooltip */
164 | button.share-link span.tooltiptext {
165 | visibility: hidden;
166 | position: absolute;
167 | left:100%;
168 | width:7.5em;
169 | font-size: 0.9em;
170 | color: #08a1c4;
171 | z-index: 5;
172 | opacity: 0;
173 | transition: all 0.2s ease-in-out;
174 | }
175 |
176 | /* adjust position of the tooltip to be beside the share button */
177 | button.share-link span.tooltiptext::after {
178 | content: "";
179 | position: absolute;
180 | bottom: 25%;
181 | right: 90%;
182 | margin-left: -0.5em;
183 | border-width: 0.4em;
184 | border-style: solid;
185 | border-color: transparent #08cfbe transparent transparent;
186 | }
187 |
188 |
189 | /* style both the final links on the home page */
190 | div.final-links {
191 | display: inline-flex;
192 | flex-flow: row wrap;
193 | width:30vw;
194 | justify-content: space-evenly;
195 | opacity: 0;
196 | transition: 0.3s opacity ease-in-out;
197 | }
198 |
199 | /* i.material-icons {
200 | padding:0;
201 | margin:0;
202 | border: 1px solid red;
203 | } */
204 |
205 | div.final-links .link {
206 | display:inline-flex;
207 | flex-flow:row nowrap;
208 | justify-content: space-evenly;
209 | align-items: center;
210 | font-size: 1.1em;
211 | line-height: 1.5em;
212 | width:12vw;
213 | text-align: center;
214 | font-weight: 500;
215 | border: 3px solid;
216 | border-image-slice: 1;
217 | color: white;
218 | border-image-source: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%);
219 | background: linear-gradient(315deg, #08a1c4 0%, #08cfbe 40%, rgb(2, 184, 117, 0.9) 100%);
220 | padding: 0em;
221 | cursor: pointer;
222 | transition: all 0.3s ease-in-out;
223 | }
224 |
225 | div.final-links .link:hover {
226 | background: transparent;
227 | color: #08a1c4;
228 | }
229 |
230 |
231 | /* media queries for the mobile screen */
232 | @media screen and (max-width:600px) {
233 | section.home {
234 | margin-top: 5em;
235 | }
236 |
237 | /* reduce font size */
238 | section.home p.error-msg {
239 | font-size: 0.9em;
240 | line-height: 1.67em;
241 | text-align: center;
242 | max-width:90vw;
243 | }
244 |
245 | /* adjust the widths of the drag-and-drop section */
246 | section.file-upload {
247 | min-width: 90vw;
248 | max-width: 95vw;
249 | min-height: 50vh;
250 | }
251 |
252 | section.file-upload div.drop-zone {
253 | padding: 0.2em;
254 | }
255 |
256 | form.file-form {
257 | text-align: center;
258 | }
259 |
260 | /* increase width of the submit button */
261 | form.file-form button.submit-btn {
262 | min-width: 40vw;
263 | width: auto;
264 | }
265 |
266 | img.submit-icon {
267 | width: 1em;
268 | height: 1em;
269 | }
270 |
271 | div.image-preview>img.preview-image {
272 | max-width: 80%;
273 | height: auto;
274 | max-height: 40vh;
275 | /* border: 2px solid;
276 | border-image-slice: 1;
277 | border-image-source: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%); */
278 | }
279 |
280 | div.progress {
281 | min-width: 60vw;
282 | max-width: 70vw;
283 | }
284 |
285 | /* change the position of the tooltip display to be underneath the share button */
286 | button.share-link span.tooltiptext {
287 | top:110%;
288 | left: 15%;
289 | width: 7em;
290 | padding:0;
291 | }
292 |
293 | button.share-link span.tooltiptext::after {
294 | bottom: 80%;
295 | right: 50%;
296 | border-color: transparent transparent #08cfbe transparent;
297 | }
298 |
299 | div.final-links {
300 | flex-flow: row nowrap;
301 | width: 80vw;
302 | justify-content: space-evenly;
303 | }
304 |
305 | div.final-links .link {
306 | justify-content: space-evenly;
307 | font-size: 1.1em;
308 | line-height: 1.67em;
309 | width: auto;
310 | padding: 0.2em;
311 | min-width: 35vw;
312 | border: 2px solid;
313 | }
314 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EasyShare
2 |
3 |
4 |
5 | A MERN stack file sharing application that uses AWS' S3 storage along with the multer-S3 node package to easily share/upload files.
6 |
7 |     [](http://makeapullrequest.com)
8 |
9 | ## Getting Started
10 |
11 | - Fork this repo and run the `git clone ` command from your terminal/bash.
12 | - `npm install` all the dependencies from the package file.
13 | - Create a `.env` file in the root of the directory and store the following keys in that file:
14 | - MONGO_URI = Insert your [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) connection URI after you create a free tier collection
15 | - S3_SECRET_ACCESS_KEY = Insert the S3 bucket's Secret access key
16 | - S3_ACCESS_KEY_ID = Insert the S3 bucket's access key ID
17 | (Note that if you choose to deploy the app, you'll need another config var called REACT_APP_BASEURL which has to be set to the deployed app's home URL)
18 | - Once all this is set up, you can choose to send a PR in case you add to the project!
19 |
20 | To get the S3 bucket credentials, you will need to have the free tier account on [AWS](https://aws.amazon.com/free/) and you may also need to set the CORS policy of the S3 bucket you create to [allow cross-origin requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html). Once you create the S3 bucket, you can create your AWS credential keys.
21 | You can obtain the MONGO_URI after create a collectoin on [mongodb atlas](https://www.mongodb.com/cloud/atlas).
22 |
23 | # Demo
24 |
25 | The app requires the user to select any file from the local storage and submit the uploaded file. Once the file upload is completed, the user is provided a download link and a shareable link. The different file formats that can are supported can be found in the **About** section of the application. The app has been deployed using Heroku's free tier plan and can be found [at this link](https://rajat-easyshare.herokuapp.com/) or in the repo description. The shareable link will be valid for 15 days after file upload, after which the document is deleted from the Database using the TTL concept.
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | # Info
39 |
40 | - The app is built using the MERN stack and uses the multer node package to handle file uploads. React hooks are used in the client-side and not class-based components.
41 | - The file formats that are supported by the app include png, jpg, jpeg, gif, webp, svg, ppt, pptx, doc, docx, pdf, xls and xlsx. The maximum size limit is set to 5 MB per file.
42 | - The app doesn't currently allow multiplt file uploads at once, instead it is designed to upload only a single file at a time.
43 | - The links provided after the successful file upload include a download link, which can download the file immediately, and a shareable link which allows the user to easily share the file at a fraction of the original filesize. The shareable link, when clicked, will lead to the original file being downloaded.
44 | - The process involved in the process of uploading and fetching the file is as follows:
45 | - The [multer package](https://www.npmjs.com/package/multer) is configured to accept file uploads with the configuration specified earlier. Further, [the multer-s3 package](https://www.npmjs.com/package/multer-s3) is used handle AWS S3 bucket file transaction.
46 | - The file is uploaded to a S3 bucket and the returned object's file location, file mimetype, key and file name are stored in a mongoDB collection using the MongoDB Atlas cloud database.
47 | - To fetch the file for download, the file key is fetched from the mongoDB collection and the corresponding file object is fetched from the S3 storage.
48 | - In order to download the file [downloadjs](https://www.npmjs.com/package/downloadjs) package is used and the buffer array is first converted in to an uInt8array.
49 | - The shareable link sends the user to another page of the app with the file ID(mongoDB object \_id) as part of the params, the same process is followed to download the file once the linked is clicked at any time.
50 | - The mongoDB objects have a TTL(time-to-live) set to 15 days, therefore the links obtained after file upload are valid only for 15 days from the time of file upload.
51 | - The files can be uploaded to the client-side using the drad-and-drop feature implemented using [react-dropzone](https://www.npmjs.com/package/react-dropzone).
52 | - The UI framework used is [materializecss](https://materializecss.com/) and the icons have been taken from [line icons](https://lineicons.com/icons/).
53 | - The UI is very straight-forward and the emphasis on the ease of usage is prominent
54 |
55 | ## Challenges faced
56 |
57 | There were a few challenges that came up during the development of the application. In this section, I aim to clarify my approach in overcoming these challeges, as a way to help you understand the code better, in case you decide to dive in!
58 |
59 | ### Handling files in Node.js
60 |
61 | Although the process of configuring multer is not that difficult, it was a challenge to understand the complete requirements. The intial configuration stored the files on the local machine,even after file upload was succssful. But in order to deploy the application using a remote web server(either through a custom ubuntu AWS server or using a PaaS like Heroku or Dokku) it was important to not use the server's storage to handle the file storage as they are usually unreliable or expensive. The solution to this issue was to use the AWS S3 Storage to store all the file objects and to only store the unique file key and corresponding details in the mongoDB collection. This allows us to have a larger file limit even on the free tier of S3 storage, since the other alternatives included implementing file storage in mongoDB itself using [GridFS](https://docs.mongodb.com/manual/core/gridfs/). The [the multer-s3 package](https://www.npmjs.com/package/multer-s3) along with multer make it very easy to setup a S3 bucket along with an [Express.js](https://expressjs.com/) server for the application. In the frontend, a drag-and-drop feature has also been used to upload files using the [react-dropzone](https://www.npmjs.com/package/react-dropzone) node package. It must be noted that in order to setup the S3 bucket along with the multer-s3 package, the CORS permission of the bucket must be set such that it allows all origins and headers to access the file objects through GET, POST, PUT and DELETE requests.
62 |
63 | ### Setting up a shareable link for file download
64 |
65 | Once the file has successfully been uploaded to the S3 storage, the app needs to provide 2 links to the user. A link to download the file immediately, and a link to share the uploaded file. Althought S3 storage provides a file URL to download the file directly, the file name usually has a date/time associated with it and the same filename is followed for the downloaded file as well. To Download the file immediately after file upload (which is assumed to be rare usage), an API call is made to the server using a GET request along with the correct file id. This API route is configured to return the MongoDB object corresponding to that file and the S3 object corresponding to this file. The S3 object contains a buffer array of the file, that needs to be converted into a [UInt8Array](https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer) so that the [downloadjs](https://www.npmjs.com/package/downloadjs) package can be used to trigger file download with the original filename instead of the S3 object's file key.
66 |
67 | In order to generate the shareable link, a seperate route is setup in the React app's App.js with the path being exact. The path has a parameter that corresponds to the \_id of the mongoDB document for the particular file. The API call is made to GET the S3 object and the mongoDB object for the required file and the file is automaticaaly downloaded using the same procedure as mentioned in the previous paragraph. Here too, the buffer array is first converted into the appropriate format to trigger downloadjs's action. Once the download is complete, the app redirects to the home page. In case a user tries to access the shared link after 15 days, a 404 error page is catch-all page.
68 |
69 | ## Potential Improvements
70 |
71 | - Allowing multiple file uploads simultaneously.
72 | - Setting up a payment gateway to allow file uploads of upto a 100MB size limit.
73 | - A user authentication setup to implement the payment gateway.
74 | - File storage for upto a year/more, upon the payment of a nominal charge.
75 | - GSAP animations for better UX.
76 | - A dashboard for registered users.
77 | - UI refactor to deal with longer file upload durations.
78 |
79 | Any more suggestions are always welcome in the PRs!
80 |
81 | ## Technologies Used
82 |
83 | Some of the technologies used in the development of this web application are as follow:
84 |
85 | - [MongoDB Atlas](https://www.mongodb.com/cloud/atlas): It provides a free cloud service to store MongoDB collections.
86 | - [React.js](https://reactjs.org/): A JavaScript library for building user interfaces. In particular, React hooks are used in the clientside of the application
87 | - [Node.js](https://nodejs.org/en/): A runtime environment to help build fast server applications using JS.
88 | - [Express.js](https://expressjs.com/): A popular Node.js framework to build scalable server-side for web applications.
89 | - [Mongoose](https://mongoosejs.com/): An ODM(Object Data Modelling)library for MongoDB and Node.js
90 | - [Heroku](http://heroku.com/): A platform(PaaS) to deploy full stack web applications for free.
91 | - [Multer](https://www.npmjs.com/package/multer) and [Multer-S3](https://www.npmjs.com/package/multer-s3): Node.js packages that help in dealing with file uploads.
92 | - [AWS S3 Storage Bucket](https://aws.amazon.com/s3/): An object storage service that offers industry-leading scalability, data availability, security, and performance.
93 | - [Materialize CSS](https://materializecss.com/): A modern responsive front-end framework based on Material Design, built by Google.
94 |
--------------------------------------------------------------------------------
/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import Dropzone from "react-dropzone"; // package to handle file drag-and-drop
3 | import axios from "axios";
4 | import download from "downloadjs"; // package to trigger file download
5 | import logo from "../stylesheets/logo.png";
6 | import "../stylesheets/home.css";
7 |
8 | // set the base URL as the localhost or the deployed URL
9 | const baseURL = process.env.REACT_APP_BASEURL || "http://localhost:5000";
10 | let frontURL = "";
11 | if (baseURL === "http://localhost:5000") {
12 | frontURL = "http://localhost:3000";
13 | }
14 |
15 | // Home Page's component
16 | const Home = () => {
17 | // Set the opacity to 1 after component mounting
18 |
19 | // Refs to handle the UI of the page
20 | const dropRef = useRef(null); // Drag-and-drop component's reference
21 | const submitBtn = useRef(null); // Submit button's reference
22 | const finalLinkRef = useRef(null); // Reference to the links displayed after file upload is done successfully
23 | const previewImgRef = useRef(null); // Reference to the preview image component
24 | const shareBtnRef = useRef(null); // Reference to the button that copies shareable link to the clipboard, after successful file upload
25 | const homeRef = useRef(null);
26 | const firstRender = useRef(true); // Reference to prevent a useEffect block from executing on componentDidMount/first render
27 |
28 | // All the state variables
29 | const [errorMsg, setErrorMsg] = useState(""); // To store any error message in the file upload process
30 | const [file, setFile] = useState({}); // To store the file object after the drag-and-drop event occurs
31 | const [previewSrc, setPreviewSrc] = useState(""); // To store the image to be previewed after file upload
32 | const [isPreviewAvailable, setIsPreviewAvailable] = useState(false); // To set a boolean variable to check if preview is available or not
33 | const [uploadedFile, setUploadedFile] = useState({}); // To store the File object returned after POST request is made to the server
34 | const [progress, setProgress] = useState(0); // To set the progress bar's width/percentage
35 | const [displayProgress, setDisplayProgress] = useState(false); // Bolean variable to display the progress bar during file upload process
36 | const [displayLinks, setDisplayLinks] = useState(false); // Boolean var to display the links (to download/copy shareable link) after successful file upload
37 |
38 | useEffect(() => {
39 | homeRef.current.style.opacity = "1";
40 | }, []);
41 |
42 | // UseEffect to handle how the progress bar works
43 | useEffect(() => {
44 | // To display the progress bar's percentage being increased gradually, during the file upload process
45 | if (progress < 100 && displayProgress) {
46 | // A setTimeout is used to avoid the 'progress' state var being changes too many times, and crashing the app because the max call-stack of react render is reached
47 | window.setTimeout(() => {
48 | setProgress(progress + 2);
49 | }, 100);
50 | }
51 | // If the progress bar is about to reach 100% but the file upload has returned the uploadedFile object
52 | else if (uploadedFile.file && progress >= 99) {
53 | // setTimeout is used to fadeout the progress bar and to fade-in the final links
54 | window.setTimeout(() => {
55 | setDisplayProgress(false); // stop displaying the progress bar
56 | setProgress(0); // set the width of the progress bar to 0
57 | setDisplayLinks(true); // display the final links to download the file/copy th shareable link to clipboard
58 |
59 | // Give time for the CSS transition to occur
60 | window.setTimeout(() => {
61 | finalLinkRef.current.style.opacity = "1";
62 | }, 100);
63 | }, 1000);
64 | }
65 | // In case the progress bar is about to reach 100%, but the file upload hasn't been completed successfuly yet
66 | // then keep the progress bar at 99%, as long as the uploadedFile object has a 'file' key that holds the correct data for the uploaded file
67 | else if (progress !== 0 && !uploadedFile.file) setProgress(99);
68 | }, [progress, displayProgress, uploadedFile]);
69 |
70 | // useEffect to handle the style changes to the drag-and-drop section, upon file being being selected using it
71 | useEffect(() => {
72 | // Do not execute this on first render
73 | if (firstRender.current) {
74 | firstRender.current = false;
75 | return;
76 | } else {
77 | // if the file selected is an image, then display its preview
78 | if (isPreviewAvailable && previewSrc) {
79 | previewImgRef.current.style.opacity = "1";
80 | }
81 | // if the file selected is not an image, then keep the height of the drag-and-drop section to be 50vh itself
82 | if (!isPreviewAvailable && file.name)
83 | dropRef.current.style.minHeight = "50vh";
84 | // if the file selected is an image, then reduce height of the drag-and-drop section to be 25vh
85 | else if (isPreviewAvailable && file.name)
86 | dropRef.current.style.minHeight = "25vh";
87 |
88 | // remove any extra styles added to it during drag event
89 | dropRef.current.style.border = "2px dashed #08a1c4";
90 | dropRef.current.style.background = "";
91 | dropRef.current.style.color = "";
92 | }
93 | }, [isPreviewAvailable, previewSrc, file]);
94 |
95 | // useEffect to handle when the submit button is displayed
96 | useEffect(() => {
97 | // If a file has been selected using the drag-and-drop component, then display the submit button
98 | if (file.name) {
99 | setErrorMsg(""); // if file is a valid mimetype, then remove any error messages displayed with a previous file selection of an unsupported mime type
100 | if (submitBtn.current) {
101 | submitBtn.current.style.visibility = "visible";
102 | submitBtn.current.style.opacity = "1";
103 | }
104 | }
105 | }, [file]);
106 |
107 | // function to update the style of the drag-and-drop component on mouseover and mouseleave
108 | const updateBorder = (dragState) => {
109 | // If the user hovers over the section, to drop a file, make the following changes to the style
110 | if (dragState === "over") {
111 | dropRef.current.style.border = "2px solid #02B875";
112 | dropRef.current.style.background =
113 | "linear-gradient(315deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%)";
114 | dropRef.current.style.color = "white";
115 | }
116 | // Once the file has been dropped, and the cursor leaves the drop-section, then remove the style changes made in the previous block
117 | else if (dragState === "leave") {
118 | dropRef.current.style.border = "2px dashed #08a1c4";
119 | dropRef.current.style.background = "";
120 | dropRef.current.style.color = "";
121 | }
122 | };
123 |
124 | // function to handle file selection using the drag-and-drop section
125 | const handleFile = (files) => {
126 | // fetch the file to be uploaded, from the argument 'files' and set it to the correct state variable
127 | const [uploadedFile] = files;
128 | setFile(uploadedFile);
129 | setDisplayLinks(false); // do not display the final links yet. Useful when user attempts consecutive file uploads
130 |
131 | // set the preview image if the file type is an image type
132 | const fileReader = new FileReader();
133 | fileReader.onload = () => {
134 | setPreviewSrc(fileReader.result);
135 | };
136 | fileReader.readAsDataURL(uploadedFile);
137 |
138 | setIsPreviewAvailable(
139 | uploadedFile.name.match(/\.(jpeg|jpg|png|webp|gif|svg)$/)
140 | );
141 | };
142 |
143 | // function to handle events after the link to share the file, is clicked
144 | const handleBtnClick = (e) => {
145 | // change the background style and color of the button
146 | shareBtnRef.current.style.background =
147 | "linear-gradient(315deg, #08a1c4 0%, #08cfbe 40%, rgb(2, 184, 117, 0.9) 100%)";
148 | shareBtnRef.current.style.color = "white";
149 |
150 | // to make it seem as though there was a click of the button, remove additional styles for 200ms
151 | window.setTimeout(() => {
152 | shareBtnRef.current.style.background = "";
153 | shareBtnRef.current.style.color = "";
154 | }, 200);
155 |
156 | // copy the correct shareable link to the clipboard
157 | navigator.clipboard.writeText(
158 | `${frontURL || baseURL}/download/${uploadedFile.file._id}`
159 | );
160 |
161 | // Display the tool tip that says 'link copied'
162 | const toolTip = document.querySelector(
163 | "button.share-link .tooltiptext"
164 | );
165 | toolTip.style.visibility = "visible";
166 | toolTip.style.opacity = "1";
167 |
168 | // remove the tooltip from DOM after 5s
169 | window.setTimeout(() => {
170 | toolTip.style.visibility = "hidden";
171 | toolTip.style.opacity = "0";
172 | }, 5000);
173 | };
174 |
175 | // function to handle file submit
176 | const handleSubmit = (e) => {
177 | e.preventDefault();
178 |
179 | // stop displaying the submit button
180 | submitBtn.current.style.opacity = "0";
181 |
182 | // if the valid filetype has been selected and set as the state var
183 | if (file) {
184 | // create a new FormData and append the file with the key 'file', as this is the key that multer has been configured to look for
185 | const formdata = new FormData();
186 | formdata.append("file", file);
187 |
188 | // Wait for CSS transition to occur before displaying the progrss bar
189 | window.setTimeout(() => {
190 | setDisplayProgress(true);
191 | }, 500);
192 |
193 | // make POST request to the server API with the headers needed to work with files/form-data
194 | axios
195 | .post(`${baseURL}/api/file/`, formdata, {
196 | headers: {
197 | "Content-Type": "multipart/form-data",
198 | },
199 | })
200 | .then((file) => {
201 | // Once the POST request occurs succesfully, the returned object includes the File object stored in the DB
202 | setErrorMsg(""); // remove any error messages
203 | submitBtn.current.style.visibility = "hidden";
204 |
205 | // Make GET request to obtain the data for the buffer array of the required file
206 | axios
207 | .get(`${baseURL}/api/file/${file.data._id}`)
208 | .then((uploadedFile) => {
209 | setDisplayLinks(true); // once the file data is obtained, display the final links
210 | setUploadedFile(uploadedFile.data);
211 | })
212 | .catch((err) => {
213 | console.log(err);
214 | });
215 | })
216 | .catch((err) => {
217 | // In case there was an error but no response is sent from the server API, it means there was an internet issue
218 | if (!err.response) {
219 | setErrorMsg(
220 | "Please connect to the internet and try again."
221 | );
222 | }
223 | // Check the error response and set the appropriate error message to be displayed to the user
224 | else {
225 | const offlineError =
226 | "Error: MongooseServerSelectionError: Could not connect to any servers in your MongoDB Atlas cluster. One common reason is that you're trying to access the database from an IP that isn't whitelisted. Make sure your current IP address is on your Atlas cluster's IP whitelist: https://docs.atlas.mongodb.com/security-whitelist/";
227 | const fileSizeError = "File too large";
228 | if (err.response.data === offlineError)
229 | setErrorMsg(
230 | "Please connect to the internet and retry."
231 | );
232 | else if (err.response.data === fileSizeError)
233 | setErrorMsg(
234 | "File is too large. Please choose a file of size < 1 MB."
235 | );
236 | else setErrorMsg(err.response.data);
237 | }
238 | });
239 | } else {
240 | setErrorMsg("Please Select a File.");
241 | }
242 | };
243 |
244 | return (
245 |
246 | {/* display any error message if it's there */}
247 |
{errorMsg}
248 |
249 | {/* the drag-and-drop section */}
250 |
339 |
340 | {/* display the links to download the file/ copy link for sharing the file, after successful file upload */}
341 | {displayLinks ? (
342 |
343 | {/* onclicking the download button, use the dowbloadjs function to trigger the file download */}
344 |
357 |
358 | {/* copy link to clipboard once this link is clicked */}
359 |
368 |