├── public ├── favicon.ico ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── images │ └── loader.svg ├── manifest.json └── index.html ├── src ├── stylesheets │ ├── logo.png │ ├── download.css │ ├── footer.css │ ├── notFound.css │ ├── navbar.css │ ├── error-404.svg │ ├── about.css │ └── home.css ├── reportWebVitals.js ├── App.css ├── index.js ├── components │ ├── NotFound.js │ ├── Navbar.js │ ├── Footer.js │ ├── Download.js │ ├── About.js │ └── Home.js ├── App.js ├── service-worker.js └── serviceWorkerRegistration.js ├── server-side ├── models │ └── file.model.js ├── server.js └── routes │ └── file.route.js ├── LICENSE ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/stylesheets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/src/stylesheets/logo.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajatm544/file-share/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/stylesheets/download.css: -------------------------------------------------------------------------------- 1 | /* style for the donwload page */ 2 | 3 | /* display a line that says 'file is downloading...' */ 4 | p.prompt { 5 | margin-top:8em; 6 | color: #08a1c4; 7 | font-family: "Poppins", sans-serif; 8 | font-size:1.2em; 9 | line-height: 1.2em; 10 | font-weight: 500; 11 | display: inline-flex; 12 | justify-content: center; 13 | align-items:center; 14 | } -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* to avoid any errors with flexbox display, set the height/width of the html tag and body tag */ 2 | html, body { 3 | background: white; 4 | margin: 0; 5 | padding:0; 6 | font-family: 'Poppins',sans-serif; 7 | } 8 | 9 | div.app { 10 | min-height: 100vh; 11 | min-width: 100vw; 12 | display: flex; 13 | flex-flow: column wrap; 14 | justify-content: space-between; 15 | } 16 | 17 | /* for mobile screens */ 18 | @media screen and (max-width:600px) { 19 | div.app { 20 | overflow-y: hidden; 21 | } 22 | } -------------------------------------------------------------------------------- /public/images/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "EasyShare", 3 | "name": "EasyShare - File", 4 | "icons": [{ 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png", 8 | "purpose": "maskable any" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }], 16 | "start_url": "/", 17 | "description": "A file sharing app built by Rajat", 18 | "display": "standalone", 19 | "theme_color": "#08a1c4", 20 | "background_color": "#08cfbe" 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/stylesheets/footer.css: -------------------------------------------------------------------------------- 1 | /* style for the footer component */ 2 | 3 | footer { 4 | width:100vw; 5 | height: 7vh; 6 | margin: 0; 7 | font-weight: 600; 8 | background: linear-gradient(135deg, hsl(191, 91%, 30%) 0%, hsl(175, 93%, 32%) 40%, hsl(158, 98%, 26%)); 9 | color: white; 10 | display: inline-flex; 11 | flex-flow: column nowrap; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | 16 | .icons { 17 | display: inline-flex; 18 | flex-flow: row wrap; 19 | justify-content: space-evenly; 20 | width:20vw; 21 | } 22 | 23 | .icons i{ 24 | color:white; 25 | font-size:1.2em; 26 | margin:0; 27 | } 28 | 29 | @media screen and (max-width: 600px) { 30 | footer { 31 | min-height: 10vh; 32 | height: auto; 33 | box-shadow: none; 34 | } 35 | 36 | .icons { 37 | width: 50vw; 38 | } 39 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | // import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | // wait 1.5s before mounting App to the DOM 8 | window.setTimeout(() => { 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | }, 1500); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://cra.link/PWA 20 | // serviceWorkerRegistration.unregister(); 21 | 22 | // If you want to start measuring performance in your app, pass a function 23 | // to log results (for example: reportWebVitals(console.log)) 24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 25 | reportWebVitals(); 26 | -------------------------------------------------------------------------------- /src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import error404 from "../stylesheets/error-404.svg"; 4 | import "../stylesheets/notFound.css"; 5 | 6 | // 404-error page component 7 | const NotFound = () => { 8 | return ( 9 |
10 | 404 11 |
12 |

Page Not Found.

13 |

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 |
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

Downloading the file...

; // display a message while the file download starts 49 | }; 50 | 51 | export default Download; 52 | -------------------------------------------------------------------------------- /src/stylesheets/navbar.css: -------------------------------------------------------------------------------- 1 | /* style for the navbar */ 2 | 3 | /* set display to flexbox and style accordingly */ 4 | nav.navbar { 5 | display: flex; 6 | flex-flow: row wrap; 7 | width:100%; 8 | position: fixed; 9 | top:0; 10 | right:0; 11 | height: 4em; 12 | justify-content: space-between; 13 | align-items: center; 14 | padding:0; 15 | margin:0; 16 | color: white; 17 | background: #08a1c4; 18 | background: linear-gradient(135deg, #08a1c4 0%, #08cfbe 40%, #02b875 100%); 19 | } 20 | 21 | nav.navbar div.nav-brand { 22 | display: inline-flex; 23 | flex-flow: row nowrap; 24 | align-items: center; 25 | justify-content: space-evenly; 26 | width:12%; 27 | margin-left: 1em; 28 | font-weight: 700; 29 | font-size:1.2em; 30 | cursor: pointer; 31 | } 32 | 33 | div.nav-brand a { 34 | width: 100%; 35 | display: flex; 36 | flex-flow: row nowrap; 37 | justify-content: space-evenly; 38 | align-items: center; 39 | } 40 | 41 | div.nav-brand img.logo { 42 | width:1em; 43 | height: 1em; 44 | } 45 | 46 | ul.nav-ul { 47 | display: flex; 48 | flex-flow: row nowrap; 49 | justify-content: space-around; 50 | align-items: center; 51 | width:10%; 52 | margin: 0; 53 | height: 80%; 54 | } 55 | 56 | /* remove any default styles for the li tags and set style as needed */ 57 | ul.nav-ul li.nav-li { 58 | list-style-type: none; 59 | font-weight: 500; 60 | cursor: pointer; 61 | padding:0; 62 | margin:0; 63 | display: inline-flex; 64 | flex-flow: row nowrap; 65 | justify-content: center; 66 | align-items: center; 67 | justify-self: center; 68 | height: 50%; 69 | width:40%; 70 | border: 1px solid transparent; 71 | 72 | } 73 | 74 | ul.nav-ul li.nav-li:hover { 75 | font-weight: 700; 76 | cursor: pointer; 77 | background: transparent; 78 | border-bottom: 1px solid white; 79 | 80 | } 81 | 82 | li.nav-li a { 83 | font-size:1.2em; 84 | } 85 | 86 | li.nav-li:hover a{ 87 | background: none; 88 | } 89 | 90 | /* for mobile screens */ 91 | @media screen and (max-width: 600px) { 92 | nav.navbar div.nav-brand { 93 | width:auto; 94 | margin-left: 1em; 95 | } 96 | 97 | div.nav-brand img.logo { 98 | margin-left:0.3em; 99 | } 100 | 101 | ul.nav-ul { 102 | width: 25%; 103 | } 104 | 105 | ul.nav-ul li.nav-li { 106 | width: auto; 107 | } 108 | } -------------------------------------------------------------------------------- /src/stylesheets/error-404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | EasyShare 12 | 17 | 23 | 29 | 30 | 31 | 35 | 36 | 40 | 44 | 45 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | Loading 68 |
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 | ![GitHub](https://img.shields.io/github/license/Rajatm544/file-share?style=flat-square) ![Heroku](https://pyheroku-badge.herokuapp.com/?app=rajat-easyshare&style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/Rajatm544/file-share?style=flat-square) ![Maintenance](https://img.shields.io/maintenance/yes/2022?style=flat-square) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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 | Laptop render 1 30 | Laptop render 2 31 |
32 | mobile render 1   33 | mobile render 2   34 | mobile render about page 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 |
251 |
252 | updateBorder("over")} 257 | onDragLeave={() => updateBorder("leave")}> 258 | {({ getRootProps, getInputProps }) => ( 259 |
262 | 263 |

264 | Drag and Drop a File
265 | or
266 | Click Here to Select a File 267 |

268 | 269 | {/* display the name of the selected file */} 270 | {file.name ? ( 271 |
272 | 273 | Selected file: 274 | {" "} 275 | {file.name} 276 |
277 | ) : ( 278 | "" 279 | )} 280 |
281 | )} 282 |
283 | 284 | {/* display any preview image or a message to say image preview isn't available */} 285 |
286 | {previewSrc ? ( 287 | isPreviewAvailable ? ( 288 |
289 | Preview 295 |
296 | ) : ( 297 |
298 |

No preview available for this file

299 |
300 | ) 301 | ) : ( 302 |
303 |

304 | Image preview will be shown here after 305 | selection 306 |

307 |
308 | )} 309 |
310 | 311 | {/* display the progress bar */} 312 | {progress > 0 ? ( 313 |
314 |
318 |
319 | ) : ( 320 | "" 321 | )} 322 |
323 | 324 | {/* display the submit button after file selection */} 325 | {file.name && !displayLinks ? ( 326 | 335 | ) : ( 336 | "" 337 | )} 338 |
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 |
369 | ) : ( 370 | "" 371 | )} 372 |
373 | ); 374 | }; 375 | 376 | export default Home; 377 | --------------------------------------------------------------------------------