未加星标

How to Create a Public File Sharing Service with Vue.js and Node.js

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二04 | 时间 2019 | 作者 红领巾 ] 0人收藏点击收藏

How to Create a Public File Sharing Service with Vue.js and Node.js

File upload plays an integral part in many web applications. It is used in programs such as email clients, chat applications, commenting systems, among others.

Before javascript frameworks dominated web development, file upload systems were similar. These systems usually comprise a form with a file input. After the form is submitted, the backend receives the files, stores them and redirects the user’s page. With the popularity of JavaScript frameworks today, the situation is different. File uploads nowadays can have several features. Some of these are AJAX submissions, progress bars, pause and continue features.

The finished code for this tutorial can be found at these Github repositories:

Frontend Client Backend Server Application Architecture

In this article, we will be building a public file upload and sharing service. It will have a Node.js-powered backend and a Vue.js-powered frontend. The service will be used anonymously and won’t have any authenticated users. We will submit the file through AJAX and store it in the backend filesystem.

The file meta-data information will be stored in a MongoDB database. All the uploaded files will be listed on the homepage of the application. Upon clicking the name of the file, we will make a request to the backend to download the file. We will also be able to delete files.

We will include a URL-shortener feature in the application to make it easier to share links. This will be achieved by generating a unique hash for each uploaded file. We will also add a mechanism to restrict which file types can be uploaded with the application.

Install and Configure Packages

Before we can install any of the packages, we will need a few things. We must have Node.js installed on our system along with the MongoDB database.

Now that we have a clear understanding of the application requirements, let's start building it. The application will have a separate backend and frontend. Each will be in a separate folder. Create two folders named client and server in the same directory. Move to the server folder and initialize a new Node.js application with:

npm init

Accept the default values when prompted. Next, move out of the server folder to the level where both folders reside. Initialize a new Vue.js application with:

vue init webpack client

Accept the default values as well. When asked to install the router plugin, select yes. Now that we have scaffolded the frontend and backend, let’s install the required packages, starting with the frontend. Move into the client folder and install the Axios.js package using the following command:

npm install --save axios

Navigate again to the server folder and, install the packages:

npm install --save btoa body-parser express mongoose multer

Let’s outline the purpose of each of the packages:

btoa : this will help us create a unique hash for a file so we can have a URL shortner functionality. body-parser : this makes it easy for the backend to access parameters from the frontend. express : this is the main backend framework built on top of Node.js mongoose : this is an ORM library. It helps to insert and manipulate data using a MongoDB database. multer : this is a library which allows us to receive and store files in the backend. List Uploaded Files

Now that the packages are installed, we will list the files from the backend. For now, we do not have any uploaded files yet but we will get to that later. In the server folder, there should be an index.js file. If it is not present, create it. In there, import several libraries by pasting the following:

const bodyParser = require('body-parser'); const express = require('express'); const app = express(); app.listen(3000, () => { console.log('Server started on port : ' + 3000); });

Start the Node.js server using:

node index.js

There should be a message in the console without any errors. The message should read:

Server started on port: 3000

Next, create a file in models/file.js . In there paste in the following:

const mongoose = require('mongoose'); const Schema = mongoose.Schema; let FileSchema = new Schema({ name: { type: String, required: true, max: 100 }, encodedName: { type: String, required: false, max: 100, default: null } }); module.exports = mongoose.model('file', FileSchema, 'files');

Here, we are using Mongoose.js to create the model to represent a single file. This is what we will use to query the database for uploaded files. To use it, we only have to import the exported module from the file.

Next, let's create a service file. This is where our logic for querying the database will be. It will also contain the connection information for the MongoDB database. Still in the server directory, create a file in services/file.service.js . In there, paste the following:

const mongoose = require('mongoose'); const File = require('../models/file'); const multer = require('multer'); const async = require('async') const fs = require('fs') const path = require('path') const btoa = require('btoa')

In the lines above, we are requiring several libraries. We have also included extra native Node.js libraries: async , fs and path . The function for async is to perform many asynchronous operations. When all operations are complete, we have one success callback. fs is used to create, delete and manipulate local files. Finally, we use path to create folder paths in a safe way depending on the environment.

Let's now connect to the database and write our method to fetch all uploaded files information in the database. Note that we will only store file information in the database. The physical file itself will be in a folder somewhere in the server. In the same file, paste in the following:

const fileConfig = require('../config/file.config') const mongoDB = fileConfig.dbConnection; mongoose.connect(mongoDB, { useNewUrlParser: true }); mongoose.Promise = global.Promise;

This connects to our database and requires the configuration information for our file service. The file does not exist yet, so let's create it in config/file.config.js . Paste in the following:

module.exports = { supportedMimes: { 'text/csv': 'csv' }, uploadsFolder: 'uploads', dbConnection: 'mongodb://127.0.0.1:27017/fileuploaddb' }

The supportedMimes config will enable us to restrict which file types we will allow for uploads. The keys in the object are the mime types and the values are the file extensions.

The uploadsFolder configuration is used to specify the directory name for uploaded files. It is relative to the server root.

In the dbConnection configuration, we are specifying the connection string for our database. The Mongoose library will create the database if it does not exist.

Finally, let us create a method for querying the files. Paste in the following into our file.service.js file:

module.exports = { getAll: (req, res, next) => { File.find((err, files) => { if (err) { return res.status(404).end(); } console.log('File fetched successfully'); res.send(files); }); } }

This exports an object with a method called getAll which fetches a list of files from the database. For now, the method only exists but isn't connected to any route so the frontend has no way of accessing it yet. Let's build our first route to fetch uploaded files.

Create a route file in routes/api.js . Add in the following:

const express = require('express'); const router = express.Router(); const fileService = require('../services/file.service.js'); const app = express(); router.get('/files', fileService.getAll); module.exports = router;

Before we start the server again, let’s paste the following into index.js :

app.use(bodyParser.json()) const apiRoutes = require('./routes/api'); app.use('/api', apiRoutes);

Before visiting the route, we need one more step. With our current setup, MongoDB database needs to be running on port 27017 . This port is the default port when the server is started without any arguments. To start the server with the default port run the command:

mongod

To start it with a specific port, use the command:

mongod --port portnumber

If you specify a port number, do not forget to update the port number in the config file, config/file.config.js in this line

dbConnection: 'mongodb://127.0.0.1:27017/fileuploaddb'

Now, the route is ready to serve files from the backend. The registered route will live at the location localhost:3000/api/files . We do not have any files in the backend yet. If we visit the URL in the browser, we will get an empty array response. In the backend console, we should notice a message titled: File fetched successfully .

Do not forget to restart the Node.js server.

Build backend API for receiving files

At this stage, the backend application is able to connect to the database. Next, we will build the route API for receiving one or more files. First, we will only store the file locally. In the file services/file.service.js , alongside the getAll method, add the following:

uploadFile: (req, res, next) => { }

This will receive the files but will not insert any information in the database yet. We will get to that in the next chapter.

In routes/api.js , add in the following line before the first route declaration:

const options = fileService.getFileOptions() const multer = require('multer')(options); router.post('/upload', multer.any(), fileService.uploadFile);

Here, we are including the multer library and providing some options for it. During the upload route declaration:

router.post('/upload', multer.any(), fileService.uploadFile);

We are specifying the multer library as a middleware. This is so that it will intercept uploaded files and do some filtering for unaccepted files. The options for the library do not exist yet. Let's add them in a method in the file services/file.service.js . Add in the method below:

getFileOptions: () => { return { storage: multer.diskStorage({ destination: fileConfig.uploadsFolder, filename: (req, file, cb) => { let extension = fileConfig.supportedMimes[file.mimetype] let originalname = file.originalname.split('.')[0] let fileName = originalname + '-' + (new Date()).getMilliseconds() + '.' + extension cb(null, fileName) } }), fileFilter: (req, file, cb) => { let extension = fileConfig.supportedMimes[file.mimetype] if (!extension) { return cb(null, false) } else { cb(null, true) } } } }

This method returns some configuration for filename construction. It will set the destination for the uploaded file. Then it filters files so that we only upload the ones specified in the file config/file.config.js .

We cannot test the upload functionality with our current setup because we have not written the frontend yet. There is a tool called postman . It is designed exactly for that.

Store File meta-data in the database

Currently, the API route for receiving files only stores the file locally. Let's modify the application so it also stores the file meta-data in the database. In the file services/file.service.js , modify the uploadFile method like below:

uploadFile: (req, res, next) => { let savedModels = [] async.each(req.files, (file, callback) => { let fileModel = new File({ name: file.filename }); fileModel.save((err) => { if (err) { return next('Error creating new file', err); } fileModel.encodedName = btoa(fileModel._id) fileModel.save((err) => { if (err) { return next('Error creating new file', err); } savedModels.push(fileModel) callback() console.log('File created successfully'); }) }); }, (err) => { if (err) { return res.status(400).end(); } return res.send(savedModels) }) }

After the multer library stores the files (locally) it will pass the file list to the callback above. The callback will create a unique hash for each file; then, it will store the file's original name and hashed key in the database.

The async part is necessary because the meta-data insertion happens asynchronously for each file. We want to return a response to the frontend only when all the information has been saved.

Any file which fails the filter test of the multer middleware will not be passed to the uploadFile callback. If no files have been uploaded, we will return an empty array to the frontend. We can then deal with any validation however we wish.

Build Frontend for listing Files

Now let's add functionality to our frontend so that it can list files from the backend. Navigate to the frontend folder. Start the development server using the command below:

npm run dev

The frontend application will be running on the URL localhost:8080 .

The first thing we need to do is allow the frontend to be able to send AJAX requests to the backend. To allow this during development, in the client root folder, let's modify the file config/index.js . Modify the proxyTable key to:

proxyTable: { '/api': 'http://localhost:3000', '/file': 'http://localhost:3000' },

We have covered more details about the above configuration in aprevious article, so check that if you’re facing any difficulties.

Let us create a component to list files. This will be responsible for fetching files from the backend. It will also loop over the list of returned files and create many instances of a child component called UploadedFile , which we will create later.

To begin with, create a component in src/components/UploadedFilesList.vue .

In there, paste in the following:

<template> <div> <h1>Files List</h1> <ul> <uploaded-file v-for="file in files" v-bind:file.sync="file"></uploaded-file> </ul> </div> </template> <script> import axios from "axios"; import UploadedFile from "./UploadedFile"; export default { name: "UploadedFilesList", data() { return { files: [] }; }, components: { UploadedFile }, methods: { fetchFiles() { let self = this; axios .get("/api/files") .then(response => { this.$set(this, "files", response.data); }); } }, mounted() { this.fetchFiles(); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style>

To list files, we are sending a request to the backend when the component is mounted. This is done using Axios.js , an HTTP library for Javascript. The component is initially created with an empty list of files. When the files list data is returned, we add it to the list of files.

Let's create the UploadedFile component.Create a component file in src/components/UploadedFile.vue . In there, paste in the following:

<template> <div> <div> <a>{{ file.name }}</a> <button>Delete</button> </div> </div> </template> <script> import axios from "axios"; export default { props: ["file"], data() { return {}; }, name: "UploadedFile", methods: {} }; </script> <style scoped> </style>

All that this component is currently doing is display the file name. The delete button does not currently perform any action but we will get to that later.

Next, let's configure the router for our application so we can display the list of files.

In the src/router/index.js file, modify the router as shown below:

import Vue from 'vue' import Router from 'vue-router' import Main from '@/components/Main' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Main', component: Main } ] })

The router is referencing the Main component file which does not exist yet. Let's create it in src/components/Main.vue . Paste in the following:

<template> <div> <h1>Anonymous File Uploader System</h1> <div> <uploaded-files ref="filesList" v-bind:files.sync="files"></uploaded-files> </div> </div> </template> <script> import axios from "axios"; import UploadedFiles from "@/components/UploadedFilesList"; export default { name: "Main", data() { return { files: [] }; }, components: { UploadedFiles }, methods: {} }; </script> <style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>

Delete the existing file src/components/Hello.vue . It was created during the scaffolding stage and we will not need it. Bootup the frontend development server using npm run dev . If there aren't any files in the backend, the list will be empty. If all goes well, we should see the text:

Files List Build Frontend for Uploading Files

At this stage, we can list files from the server. Our next task is to add the ability to upload files.

First, create a component for uploading files in src/components/UploadsContainer.vue . In there, paste the following:

<template> <div class="hello"> <h1>Uploader</h1> <div> <input v-show="!uploadStarted" type="file" multiple v-bind:name="uploadName" @change="fileSelected" > <p v-show="uploadStarted">Uploading...</p> </div> <div> <button v-show="!uploadStarted" v-on:click="startUpload">Start Upload</button> <button v-show="uploadStarted" v-on:click="cancelUpload">Cancel Upload</button> </div> </div> </template> <script> import axios from "axios"; const CancelToken = axios.CancelToken; const source = CancelToken.source(); export default { name: "Main", data() { return { uploadStarted: false, uploadName: "files", uploadUrl: "/api/upload", formData: null }; }, methods: { fileSelected(event) { if (event.target.files.length === 0) { return; } let files = event.target.files; let name = event.target.name; let formData = new FormData(); for (let index = 0; index < files.length; index++) { formData.append(name, files[index], files[index].name); } this.$set(this, "formData", formData); }, startUpload() { this.$set(this, "uploadStarted", true); this.uploadData(this.formData); }, cancelUpload() { if (this.uploadStarted) { source.cancel(); } this.$set(this, "uploadStarted", false); }, uploadData(formData) { if (this.formData === null) { return; } axios .post(this.uploadUrl, formData, { cancelToken: source.token }) .then(response => { if (response.data.length === 0) { alert("File not uploaded. Please check the file types"); return; } this.updateFilesList(response.data); this.$set(this, "formData", null); }) .catch(() => { alert("Error occured"); }) .then(() => { this.$set(this, "uploadStarted", false); }); }, updateFilesList(files) { this.$emit("files-uploaded", files); } } }; </script> <style scoped> </style>

Add it to the dependencies of src/components/Main.vue as shown below. First, let us import it:

import UploadsContainer from '@/components/UploadsContainer'

Then we list it as a child component:

components: { UploadedFiles, UploadsContainer },

Add a method as shown below:

methods: { filesUploaded(files) { this.$refs.filesList.filesUploaded(files) } }

Then, instantiate it in the template as shown below:

<template> <div class="hello"> <h1>Anonymous File Uploader System</h1> <div> <uploads-container v-on: files-uploaded="filesUploaded"></uploads-container> </div> <div> <uploaded-files ref="filesList" v-bind: files.sync="files"></uploaded-files> </div> </div> </template >

In the component src/components/UploadedFilesList.vue , add a method as below:

filesUploaded(files) { files.forEach(file => { this.files.push(file) }) }

Let us break down what is happening in these components.

Inside src/components/UploadsContainer , we have a file upload input. Attached to it is a changed event handler called fileSelected :

@change="fileSelected"

When a file is selected, this handler is fired. The logic in this handler sets the selected files as a property in the component using the following:

let formData = new FormData() for (let index = 0; index < files.length; index++) { formData.append(name, files[index], files[index].name) } this.$set(this, 'formData', formData)

This is using html5's native FormData API.

Then we have a submit button:

<button v-show="!uploadStarted" v-on:click="startUpload">Start Upload</button>

This calls a method named startUpload which is responsible for setting the status as actively uploading. Then, it calls another method which sends the formData property, containing the files to the backend.

If the upload was successful, we set the formData to null. Then, we emit an event to the parent container so it can update the uploaded files list using:

updateFilesList (files) { this.$emit("files-uploaded", files) }

If an error occurs, we show an alert to the user. We also have a cancel feature which will be triggered by the cancel button below:

<button v-show="uploadStarted" v-on:click="cancelUpload">Cancel Upload</button>

And it will only show when an upload process has started. The “start upload” button will only display when there is no upload in progress.

We are binding the form field to a property which specifies the key that will be used when sending files to the server:

v-bind:name="uploadName"

The input field will also be hidden when an upload is in progress.

Onto the next file src/components/Main.vue . After instantiating UploadsContainer , we listen to an event using the syntax:

v-on:files-uploaded="filesUploaded"

This will receive the uploaded files so we can pass it to a method named filesUploaded in the component src/components/UploadedFilesList.vue . This will make sure the list is updated.

Add support for file download Frontend download setup

Now that we have the ability to upload files, let's make sure we can download them.

First, create a component in src/components/FileDownloader.vue .

In there, paste the following:

<template> <iframe v-bind:src="source"></iframe> </template> <script> export default { data() { return { source: "" }; }, methods: { downloadFile(source) { this.$set(this, "source", source); } } }; </script>

This component includes an iframe in the template. Anytime the source for the iframe changes, it will make a request to that URL.

In the component src/components/UploadedFile.vue , include the downloader:

import FileDownloader from './FileDownloader'

Let us register it first:

components: { FileDownloader },

Then we can use it in the template:

<file-downloader :key="downloadKey" ref="downloader"></file-downloader>

Add a method:

downloadFile(event) { event.preventDefault() let url = event.target.href this.downloadKey += 1 this.$nextTick().then(() => { this.$refs.downloader.downloadFile(url) }) }

Then, modify the link in the template as shown below:

<a v-bind:href="'/file/download/' + file.encodedName" v-on:click="downloadFile">{{ file.name }}</a>

This generates the appropriate URL by binding to the encodedName property of our file props.

Let's make sure that the download is triggered on every click. We have to bind the download component's key to a data property on the parent component.

Add a data property in src/components/UploadedFile.vue :

return { downloadKey: 1 }

This key is incremented on each click of the download link. This forces the iframe to rerender and hence triggers the download.


How to Create a Public File Sharing Service with Vue.js and Node.js
Backend download setup

Now, the frontend is ready for making download requests. However, the backend has not been set up to serve the files yet. Let's set it up now. In the backend file index.js , add the following lines before the call to start the server:

const fileRoute = require('./routes/file'); app.use('/file', fileRoute);

Next, create the route file routes/file.js . In there, add the content:

const express = require('express'); const router = express.Router(); const fileService = require('../services/file.service.js'); router.get('/download/:name', fileService.downloadFile); module.exports = router;

This sets up a route which accepts the hashed key of a file as an argument. This argument is then used to fetch the file from the database to get the real name of the file. Then, we reply with a download response.

Let’s set up the method handler for the route. Inside the file services/file.service.js , add a method to the exports as shown:

downloadFile(req, res, next) { File.findOne({ name: req.params.name }, (err, file) => { if (err) { res.status(400).end(); } if (!file) { File.findOne({ encodedName: req.params.name }, (err, file) => { if (err) { res.status(400).end(); } if (!file) { res.status(404).end(); } let fileLocation = path.join(__dirname, '..', 'uploads', file.name) res.download(fileLocation, (err) => { if (err) { res.status(400).end(); } }) }) } }) }

When we restart the backend server, any file link on the frontend can now be clicked to download that file.

Add Frontend support for deleting files

Finally, let's add functionality to delete files. Let's work on the frontend first. In the frontend file src/components/UploadedFile.vue , add the method below:

deleteFile (file) { this.$emit("delete-file", file); },

Modify the delete button in the component to the following:

<button v-on:click="deleteFile(file)">Delete</button>

Upon clicking the button, the component emits an event called delete-file to the parent.

Let's modify the parent component src/components/UploadedFilesList.vue . Modify the UploadFile instantiation to the following:

<uploaded-file v-for="file in files" v-bind:file.sync="file" v-on:delete-file="deleteFile" ></uploaded-file>

In there, we add an event listener for the emitted child event we just made. This in turn calls a method named deleteFile in the parent. Let's create that method:

deleteFile(file) { if (confirm('Are you sure you want to delete the file?')) { axios.delete('/api/files/' + file._id) .then(() => { let fileIndex = this.files.indexOf(file) this.files.splice(fileIndex, 1) }) .catch(() => { console.log("Error deleting file") }) }

The frontend is ready to send AJAX requests to the backend.

Let's set up the backend to receive the request. In the backend file routes/api.js , add the following line just before the export statement:

router.delete('/files/:id', fileService.deleteFile);

Then, in the file services/file.service.js , add the method below:

deleteFile(req, res, next) { File.findOne({ id: req.params._id }, (err, file) => { if (err) { res.status(400).end(); } if (!file) { res.status(404).end(); } let fileLocation = path.join(__dirname, '..', 'uploads', file.name) fs.unlink(fileLocation, () => { File.deleteOne(file, (err) => { if (err) { return next(err) } return res.send([]) }) }) }) },

Now, we can delete files. When we click the delete link, we get an alert to confirm. If we click “ok”, the file is deleted from the backend folder and the information is removed from the database.

Conclusion

That brings us to the end of our article. We created a file upload service which is capable of many file uploads. It enables us to delete the files and we can download the file as well.

This is only a basic upload application. Possible expansions to this application could be advanced validation, upload progress, image preview feature, or multiple file downloads. Hopefully, this brought you some inspiration and ideas. As usual, if there are any questions, please tweet them directly to the author at @LaminEvra .

Also, if you’re building an app with important logic and want to prevent tampering or reverse-engineering, see how to protect it with Jscrambler .

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:How to Create a Public File Sharing Service with Vue.js and Node.js
本站链接:https://www.codesec.net/view/628602.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(290)