preface

Whether you’re using someone else’s web page or your own design, one feature that’s pretty common is uploading files. Common examples include images (.jpg.png), audio (.mp.mp4), documents (.doc), etc. It’s usually the front and back end, but you know there’s NodeJS on the front end, so get to work. (There must be a more powerful solution, more powerful code, what better implementation, you can say in the comments, it is best to directly on the code)

The sample show

Take the environment

Let’s stick with the slightly familiar NodeJS framework -KOA2

Install dependencies

npm install koa --save
npm install koa-router --save
npm i formidable --save
npm install koa-static --save
npm i @koa/cors --save
Copy the code

The test environment

The environment is fine

var Koa = require('koa'); var app = new Koa(); var Router = require('koa-router')(); const fs = require('fs') const formidable = require('formidable') const path = require('path') const koaStatic = require('koa-static') app.use(koaStatic(path.join(__dirname, Const cors = require("@koa/cors") app.use(cors()) app.use(async (CTX,)) next) => { ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); if (ctx.method == 'OPTIONS') { ctx.body = 200; } else { await next(); }}); Router.get('/', async (CTX) => {ctx.body = "OK"}) app.use (router.routes ()) // Start router.use (router.allowedMethods ()); app.listen(3000);Copy the code

Ideas and Implementation

The front-end thinking

We use the slice method of Blob to split the binary file. We can use Axios to upload the file, and then we have a control to pause and continue the file, which can be understood as canceling the request and resending the request, but when resending the request, we need to determine which slices were uploaded. (The code is a bit long, so try to comment it.)

The front-end code

<template> <input type="file" @change="onChange" ref="img" /> <button @click="onContinue" </button> <div class="progress"></div> </div> </template> <script > import { onMounted, ref } from "vue"; import axios from "axios"; // Prepare for the following cancellation request const CancelToken = axios.canceltoken; Export default {name: "App", setup() {let img = ref(); / / used to calculate the progress bar (alreadyUpload/uploadTotal) * 100 let alreadyUpload = ref (0); let uploadTotal = ref(1); // Let cancel = ref([]); let isChange = ref(false); async function onChange() { let sum = []; let file = img.value.files[0]; let size = 1024 * 20; Let fileChunks = []; let index = 0; IsChange. Value = true; / / get uploaded slice await axios ({method: "get", url: "http://127.0.0.1:3000/alreadyFile", params: {filename: File.name,}, // cancelToken: new cancelToken (function executor(c) {cancel.value.push(c); }),}). Then ((response) => {sum = response.data; alreadyUpload.value = sum.length; }) // eslint-disable-next-line @typescript-eslint/no-empty-function .catch(() => {}); For (let cur = 0; cur < file.size; cur += size) { fileChunks.push({ hash: index++, chunk: file.slice(cur, cur + size), }); } uploadTotal.value = fileChunks.length; If (sum. Length! = fileChunks.length) { for (let i = 0; i < fileChunks.length; i++) { let item = fileChunks[i]; let formData = new FormData(); formData.append("filename", file.name); formData.append("hash", String(item.hash)); formData.append("chunk", item.chunk); If (sum.indexof (item.hash) == -1) {// Upload a slice axios({method: "post", url: "Http://127.0.0.1:3000/upload", data: formData, cancelToken: new CancelToken(function executor(c) { cancel.value.push(c); }), }) .then(() => { alreadyUpload.value = alreadyUpload.value + 1; }) // eslint-disable-next-line @typescript-eslint/no-empty-function .catch(() => {}); } else { continue; }}} else {console.log(" upload completed "); } / / request file link await axios ({method: "get", url: "http://127.0.0.1:3000/fileLink", params: {filename: file.name, }, cancelToken: new CancelToken(function executor(c) { cancel.value.push(c); }),}).then((response) => {// Return file link console.log(response.data); }) // eslint-disable-next-line @typescript-eslint/no-empty-function .catch(() => {}); } // Click the pause button const noSuspend = () => {console.log(" request canceled "); cancel.value.forEach((val) => { val(); }); }; Const onContinue = () => {if (ischange.value) {onChange(); }}; return { img, onChange, alreadyUpload, uploadTotal, noSuspend, onContinue, }; }}; </script> <style scoped> .progress-frame { width: 500px; height: 32px; background-color: aqua; } .progress { width: v-bind("(alreadyUpload/uploadTotal)*100+'%'"); height: 32px; background-color: red; } </style>Copy the code

Back-end ideas and code

This place together says, on the back end we prepare two folders one for holding slices of the same file, and one for holding the merged files of slices (i.e. the files returned by the link).

Configure the path in the global, one is the slice file directory, one is the slice merged file directory

const TEMPORARY_FILES = path.join(__dirname, 'temporary')
const STATIC_FILES = path.join(__dirname, 'public')
Copy the code

Get uploaded slices

Add a route that returns an array of names for uploaded slices

Router.get("/alreadyFile", async (ctx) => { const { filename } = ctx.query; Const already = [] //fs.readdirSync, which returns an array object containing the names of all files in the specified directory // If (! fs.existsSync(`${TEMPORARY_FILES}\\${filename}`)) { ctx.body = already; } else {// there are folders, Fs.readdirsync (' ${TEMPORARY_FILES}\\${filename} ').foreach ((name) => {already-push (Number(name))}) ctx.body = already } })Copy the code

Returns an array of strings from 0 to 18 [‘1′,’2′,’3′,… ’18’]

Uploading of slices

Add the routing

The main function is to create slices, and put into the same folder, and then put the folder into the slice directory. That’s the point.

Router.post('/upload', async (ctx) => { let form = new formidable.IncomingForm(); form.parse(ctx.req, (err, value, Let dir = '${TEMPORARY_FILES}\\${value.filename}' // Number of slices let hash = value.hash; let chunk = files.chunk; Const buffer = fs.readfilesync (chunk.filepath) try {// Whether the folder exists if (! Fs.existssync (dir)) {// Create fs.mkdirsync (dir)} // Create a slice file const ws = fs.createWritestream (' ${dir}\\${hash} ') // Slice write ws.write(buffer) ws.close() } catch (error) { console.error(error) } }) ctx.body = "ok" })Copy the code

Back to file link

Add the routing

The main function is to check whether the file has been merged, if there is a link to return to the file, otherwise merge slices first and then return to the file link.

Router.get('/fileLink', async (CTX) => {const {filename} = ctx.query try { Ctx. origin if (fs.existssync (' ${STATIC_FILES}\\${filename} ')) {ctx.body = {"url": `${ctx.origin}/${filename}` } } } catch (error) { console.error(error); } try {let len = 0 //fs.readdirSync, which returns an array object containing the names of all files in the specified directory const bufferList = fs.readdirSync(`${TEMPORARY_FILES}\\${filename}`).map((hash, Const buffer = fs.readFilesync (' ${TEMPORARY_FILES}\\${filename}\\${index} ') len += buffer.length return buffer }); Const Buffer = buffer. concat(bufferList, len); // Create file under public const ws = fs.createWritestream (' ${STATIC_FILES}\\${filename} ') ws. Write (buffer); ws.close(); } catch (error) { console.error(error); } ctx.body = { "url": `${ctx.origin}/${filename}` } })Copy the code

conclusion

Sliced file uploads are pretty much like this, but there are still a lot of problems, such as how to deal with two files with the same name, and large files with too many slices, which will send many requests at the same time, etc. Have understand can comment to say, the blogger also is not very understand. For the file slice upload, said simple understanding of the feeling is very simple, said it is also very difficult, this code without someone’s guidance can not come out may be dish.