nodejs+koa2实现文件上传
前言
最近由于项目需要,研究了一下JS中的文件上传,并做了一下简单的总结和梳理。下面将对该功能分两部分(服务端和客户端)进行分别介绍。本文将只介绍服务端部分。一、环境准备及第三方库
- 我们将采用nodejs+koa2来实现文件上传的服务端部分
- nodejs版本:v12.16.1
- koa版本:v2.13.1
- formidable:v1.2.2 用于解析post请求中传递的参数,Content-Type为:application/x-www-form-urlencoded
- koa-router:v10.0.0 用于配置路由
- koa-static: v5.0.0 读取静态文件中间件
- koa2-cors: v2.0.6 用于cors跨域设置
- multiparty:v4.2.2 用于解析和上传form-data文件
- spark-md5:v3.0.1 用于根据文件内容生成字符串(相同内容的文件生成的值是一样的)
- nodemon:v2.0.7 用该命令运行js文件可自动重启服务
二、项目结构
server:根目录
- node_modules:自动生成,存放所有依赖库
- upload:用于存放上传后的文件
- server.js:api接口文件,定义所有的上传接口
- package.json:项目的配置文件
三、API接口说明(server.js)
1.基本配置
- 首先我们先将所有要用到的库在文件的头部引入
- 配置一些全局属性,如服务器地址、端口号、文件保存路径等等
- 因为是前后端分离,所有可能会涉及到跨域请求,需要配置cors跨域
- 最后再应用一下koastatic中间件用于访问静态资源文件
整体骨架代码如下:
const Koa = require('koa');
const Router = require('koa-router');
const koastatic = require('koa-static');
const fs = require('fs');
const formidable = require('formidable');
const multiparty = require('multiparty');
const SparkMD5 = require('spark-md5');
const path = require('path');
const app = new Koa();
let router = new Router();
//中间件:设置允许跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
//处理OPTIONS请求
ctx.request.methods === 'OPTIONS' ? ctx.body = '请求测试成功' : await next();
});
const host = '127.0.0.1',
port = 3000;
const HOSTNAME = `${
host}:${
port}`;
const SERVER_PATH = `${
__dirname}/upload`;
app.listen(port, function () {
console.log('======================================================================');
console.log(`The server started at port: ${
port}, you can access it by ${
HOSTNAME}`);
console.log('======================================================================');
});
//.......
//这里添加文件上传的API接口
//.......
app.use(koastatic('./'));
app.use(router.routes());
app.use(router.allowedMethods());
2. 公共函数封装
在实现各个接口前难免会涉及到一些重复的代码,这个时候我们就需要进行一些必要的封装,以实现代码的复用。
接下来我们就一起来分析并封装一下常用的方法
- 检测文件是否存在
- 本案例中我们采用nodejs内置的fs模块的access函数来检测文件是否已经存在
- 利用Promise进行管理
- 关键代码:fs.access(filepath, fs.constants.F_OK, err=>{…})
- 另:fs模块中有个exists方法也可以用于检测文件是否存在,但官方文档中已经弃用,并推荐使用access方法。
//检测文件是否已经存在
const exists = function exists(path){
return new Promise((resolve, reject)=>{
fs.access(path, fs.constants.F_OK, err=>{
if(err){
resolve(false);
return;
}
resolve(true);
});
});
}
- 利用multiparty解析请求中的文件信息
- 利用该插件可实现form-data格式的请求中的文件解析和提取
- 该模块配置项中有个uploadDir属性,如果指定则会自动上传文件
- 因为本文还将涉及到自定义文件名上传文件,所以这里我们将该方法封装为可配置的
- 关键代码:new multiparty.From(config).parse(req,(err, fields, files)=>{})
//利用multiparty插件解析前端传来的form-data格式的数据,并上传至服务器
const multipartyUpload = function multipartyUpload(req, autoUpload){
let config = {
maxFieldsSize : 200 * 1024 *1024
}
if(autoUpload) config.uploadDir = SERVER_PATH;
return new Promise((resolve,reject)=>{
new multiparty.Form(config).parse(req, (err, fields, files)=>{
if(err){
reject(err);
return;
}
resolve({
fields,
files
});
});
});
}
- 将文件内容写入服务器
- 该方法中我们将以两种方式进行文件的写入
- 一种是:对于前端传过来的form-data格式的文件我们将以流的方式进行写入
- 另一种:则是将内容直接写入到文件,比如BASE64格式的图片文件
- 这里我们将用到内置模块fs中的createReadStream和createWriteStream用于以流的方式写入,还有fs中的writeFile用于将内容直接写入
- 关键代码: fs.createReadStream(filepath); fs.createWriteStream(serverpath); readStream.pipe(writeStream); fs.writeFile(serverpath,file,err=>{…})
//将传进来的文件数据写入服务器
//form-data格式的数据将以流的形式写入
//BASE64格式数据则直接将内容写入
const writeFile = function writeFile(serverPath, file, isStream){
return new Promise((resolve, reject)=>{
if(isStream){
try{
let readStream = fs.createReadStream(file.path);
let writeStream = fs.createWriteStream(serverPath);
readStream.pipe(writeStream);
readStream.on('end',()=>{
resolve({
result: true,
message: '上传成功!'
});
fs.unlinkSync(file.path);
});
}catch(err){
resolve({
result: false,
message: err
})
}
}else{
fs.writeFile(serverPath,file, err=>{
if(err){
resolve({
result: false,
message: err
})
return;
}
resolve({
result: true,
message: '上传成功!'
});
});
}
});
}
- post请求中application/json或application/x-www-form-urlencoded格式的参数解析
- 对于from-data形式的文件信息我们可以用multiparty插件进行解析和提取,但是对于其它格式的一些参数,multiparty就无法解析到了
- 这里我们利用formidable进行解析post参数
- formidable可以将a=xxx&b=zzz格式的参数解析为json格式,前端请求时需用qs进行相应的格式处理
- 另外:koa-bodyparser也可以用来解析,但是尝试用了一下都没有成功,所以改用formidable
- 关键代码:new formidable.IncomingForm().parse(req,(err, fields)=>{…})
//解析post请求参数,content-type为application/x-www-form-urlencoded 或 application/josn
const parsePostParams = function parsePostParams(req){
return new Promise((resolve, reject)=>{
let form = new formidable.IncomingForm();
form.parse(req,(err,fields)=>{
if(err){
reject(err);
return;
}
resolve(fields);
});
});
}
- 大文件切片上传后再合并
- 在后面要讲的大文件切片上传,断点续传中,一个大文件会被切成n多个小文件上传
- 在上传后我们需要再将这些切片文件合并成一个文件,从而实现大文件上传
- 主要利用内置fs模块中的appendFileSync和readFileSync方法
- 关键代码:fs.appendFileSync(serverFilePath, fs.readFileSync(filePath))
const mergeFiles = mergeFiles(hash, count){
return new Promise((resolve, reject)=>{
const dirPath = `${
SERVER_PATH}/${
hash}`;
if(!fs.existsSync(dirPath)){
reject('还没上传文件,请先上传文件');
return;
}
const filelist = fs.readdirSync(dirPath);
if(filelist.length < count){
reject('文件还未上传完成,请稍后再试');
return;
}
let suffix;
filelist.sort((a,b)=>{
const reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
}).forEach(item =>{
!suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1]: null;
//将每个文件读取出来并append到以hash命名的新文件中
fs.appendFileSync(`${
SERVER_PATH}/${
hash}.${
suffix}`, fs.readFileSync(`${
dirPath}/${
item}`));
fs.unlinkSync(`${
dirPath}/${
item}`);//删除切片文件
});
await delay(1000);//等待1秒后删除新产生的文件夹
fs.rmdirSync(dirPath);
resolve({
path:`${
HOSTNAME}/upload/${
hash}.${
suffix}`,
filename: `${
hash}.${
suffix}`;
})
});
}
- 延迟函数
//定义延迟函数
const delay = function delay(interval) {
typeof interval !== 'number' ? interval = 1000 : null;
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve();
}, interval);
});
}
3. 文件上传接口
- 上传form-data格式的单个文件
- API URL:/upload_single_file
- 请求方式:post
- Content-Type:form-data
- 参数:FormData对象,如:new FormData().append({file: [file]})
- 返回值:application/json {code:0/1, message:’’, filename,filepath}
- 这个接口比较简单,我们可以利用multiparty插件对request进行文件信息的解析和上传
- multiparty配置中有个uploadDir属性,如果给这个属性指定了值,那么文件解析完成后会自动上传到该路径下
- multiparty在自动上传文件时会重新生成文件名后上传
- 由于前面我们已经利用multiparty封装好了解析和上传的公共方法,所以这里直接调用即可
//上传单个文件(form-data),利用第三方插件multipary解析并上传
router.post('/upload_single_file', async (ctx, next) => {
try {
let {
files
} = await multipartyUpload(ctx.req, true);
let file = (files && files.file.length) ? files.file[0] : {
};
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: file.originalFilename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
} catch (err) {
ctx.body = {
code: 1,
message: '文件上传失败'
}
}
});
- 上传form-data格式的单个文件,并且由前端定义文件名
- api:/upload_single_formdata_rename
- 请求方式:post
- 参数:FormData对象,如:new FormData().append({file: [file]})
- Content-Type:form-data
- 返回值:application/json {code:0/1, message:’’, filename,filepath}
- 这个接口与上面的类似,但不同的是我们需要用前端传给我们的文件名,而不是用multiparty默认生成的
- 这时就不能用multiparty的自动上传功能了,只需用multiparty解析出文件信息和文件名即可
- 然后用我们自己封装好的writeFile方法来将文件以流的形式写入到服务器上
//上传单个文件(form-data),利用第三方插件解析但不直接上传,而是将文件重命名后再单独上传
router.post('/upload_single_formdata_rename', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file.length) ? files.file[0] : {
};
let filename = (fields && fields.filename.length) ? fields.filename[0] : '';
const filePath = `${
SERVER_PATH}/${
filename}`;
let isExist = await exists(filePath);
if (isExist) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filePath, file, true);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (ex) {
ctx.body = {
code: 0,
message: ex
}
}
});
- 上传BASE64格式的单个图片
- api:/upload_base64
- 请求方式:post
- 参数:file=base64xxxx&filename:xxx
- Content-Type:application/x-www-form-urlencoded
- 返回值:application/json {code:0/1, message:’’, filename,filepath}
- 此接口仅用于上传较小的图片文件,图片过大不建议使用该接口,会导致内存消耗程序卡死
- 需要利用formidable将参数解析出来,可以直接调用我们前面封装好的parsePostParams方法
- 参数解析出来后,再利用SparkMD5基于文件内容重新生成文件名并上传
- 调用上面封装的writeFile方法直接将base64格式的内容写入文件
//BASE64上传,该方式只能上传小图片,大图片不建议使用这种方式会造成程序卡死,大图片使用form-data上传
router.post('/upload_base64', async (ctx, next) => {
try {
let {
file,
filename
} = await parsePostParams(ctx.req);
file = decodeURIComponent(file);
const suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1];
let spark = new SparkMD5.ArrayBuffer();
file = file.replace(/^data:image\/\w+;base64,/, "");
file = Buffer.from(file, 'base64');
spark.append(file);
let filepath = `${
SERVER_PATH}/${
spark.end()}.${
suffix}`;
await delay();
const isExists = await exists(filepath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filepath, file, false);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: filepath.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (err) {
console.log(err);
ctx.body = {
code: 0,
message: err
}
}
});
- 大文件切片上传
- api:/upload_chunk
- 请求方式:post
- 参数:FormData
- Content-Type:form-data
- 返回值:application/json {code:0/1, message:’’, filename,filepath}
- 在前端调用接口前会把大文件切成若干个小文件,然后按照“文件名_数字.xxx”的格式编号后分别传给服务端
- 服务器接收到文件后先解析出文件名(不包含数字部分),并以文件名命名创建一个临时目录用于存放所有的切片文件
- 由于是form-data个的,同样需要借助multiparty进行文件信息解析
- 最后调用writeFile方法以流的方式将切片文件保存到临时目录中
//大文件切片上传
router.post('/upload_chunk', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file[0]) || {
};
let filename = (fields && fields.filename[0]) || '';
let [, hash] = /^([^_]+)_(\d+)/.exec(filename);
const dirPath = `${
SERVER_PATH}/${
hash}`;
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const filePath = `${
dirPath}/${
filename}`;
const isExists = await exists(filePath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: filePath.replace(__dirname, HOSTNAME)
}
return;
}
await wirteFile(filePath, file, true);
ctx.body = {
code: 0,
message: '文件上传成功',
serverPath: filePath.replace(__dirname, HOSTNAME)
}
} catch (err) {
ctx.body = {
code: 1,
message: err
}
}
});
- 合并所有切片并删除临时目录和切片文件
- api:/upload_merge
- 请求方式:post
- 参数:application/json {hash,count}
- Content-Type:application/x-www-form-urlencoded
- 返回值:application/json {code:0/1, message:’’, filename,filepath}
- 在所有的切片文件上传完成后,根据切片的数据和文件的名称(不包含数字部分)将所有切片合并成一个大文件,
- 切片合并完成后删除所有的切片文件和临时目录
- 调用parsePostParams解析出文件名(hash)和切片数量(count)参数
- 调用mergeFiles合并所有切片
//合并切片文件
router.post('/upload_merge', async (ctx, next) => {
const {
hash,
count
} = await parsePostParams(ctx.req);
const {
path,
filename
} = await mergeFiles(hash, count);
ctx.body = {
code: 0,
message: '文件上传成功',
path,
filename
}
});
- 获取已上传的切片文件,可用于断点续传
- api:/uploaded
- 请求方式:get
- 参数:application/json {hash}
- Content-Type:application/x-www-form-urlencoded
- 返回值:application/json {code:0/1, message:’’, filelist}
- 在上面的切片上传接口中,我们会以文件名(不含数字部分)生成一个临时目录用于保存所有的切片
- 在每次大文件切片上传前都应先调用一个该接口,查看是否有已经上传的切片,如果已经上传就直接跳过,这样既可实现断点续传
//获取已上传的切片
router.get('/uploaded', async (ctx, next) => {
try {
const {
hash
} = ctx.request.query;
const dirPath = `${
SERVER_PATH}/${
hash}`;
const filelist = fs.readdirSync(dirPath);
filelist.sort((a, b) => {
const reg = /_([\d+])/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
ctx.body = {
code: 0,
message: '获取成功',
filelist
}
} catch (err) {
ctx.body = {
code: 0,
message: '获取失败'
}
}
});
总结
- 以上全部就是文件上传的接口说明及实现,真实项目中可以根据不同的需求调用不同的接口,下一篇文章中我们将介绍一下前端部分的实现和对接口的调用
- 下面附上服务器端的完整代码:
const Koa = require('koa');
const Router = require('koa-router');
const koastatic = require('koa-static');
const fs = require('fs');
const formidable = require('formidable');
const multiparty = require('multiparty');
const SparkMD5 = require('spark-md5');
const path = require('path');
const app = new Koa();
let router = new Router();
//中间件:设置允许跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
//处理OPTIONS请求
ctx.request.methods === 'OPTIONS' ? ctx.body = '请求测试成功' : await next();
});
const host = '127.0.0.1',
port = 3000;
const HOSTNAME = `${
host}:${
port}`;
const SERVER_PATH = `${
__dirname}/upload`;
app.listen(port, function () {
console.log('======================================================================');
console.log(`The server started at port: ${
port}, you can access it by ${
HOSTNAME}`);
console.log('======================================================================');
});
//定义延迟函数
const delay = function delay(interval) {
typeof interval !== 'number' ? interval = 1000 : null;
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve();
}, interval);
});
}
//检测文件是否已经存在
const exists = function exists(path) {
return new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) {
resolve(false);
return;
}
resolve(true);
});
});
}
//利用multiparty插件解析前端传来的form-data格式的数据,并上传至服务器
const multipartyUpload = function multipartyUpload(req, autoUpload) {
let config = {
maxFieldsSize: 200 * 1024 * 1024
}
if (autoUpload) config.uploadDir = SERVER_PATH;
return new Promise((resolve, reject) => {
new multiparty.Form(config).parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
resolve({
fields,
files
});
});
});
}
//将传进来的文件数据写入服务器
//form-data格式的数据将以流的形式写入
//BASE64格式数据则直接将内容写入
const writeFile = function writeFile(serverPath, file, isStream) {
return new Promise((resolve, reject) => {
if (isStream) {
try {
let readStream = fs.createReadStream(file.path);
let writeStream = fs.createWriteStream(serverPath);
readStream.pipe(writeStream);
readStream.on('end', () => {
resolve({
result: true,
message: '上传成功!'
});
fs.unlinkSync(file.path);
});
} catch (err) {
resolve({
result: false,
message: err
})
}
} else {
fs.writeFile(serverPath, file, err => {
if (err) {
resolve({
result: false,
message: err
})
return;
}
resolve({
result: true,
message: '上传成功!'
});
});
}
});
}
//上传单个文件(form-data),利用第三方插件multipary解析并上传
router.post('/upload_single_file', async (ctx, next) => {
try {
let {
files
} = await multipartyUpload(ctx.req, true);
let file = (files && files.file.length) ? files.file[0] : {
};
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: file.originalFilename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
} catch (err) {
ctx.body = {
code: 1,
message: '文件上传失败'
}
}
});
//上传单个文件(form-data),利用第三方插件解析但不直接上传,而是将文件重命名后再单独上传
router.post('/upload_single_formdata_rename', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file.length) ? files.file[0] : {
};
let filename = (fields && fields.filename.length) ? fields.filename[0] : '';
const filePath = `${
SERVER_PATH}/${
filename}`;
let isExist = await exists(filePath);
if (isExist) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filePath, file, true);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (ex) {
ctx.body = {
code: 0,
message: ex
}
}
});
//解析post请求参数,content-type为application/x-www-form-urlencoded 或 application/josn
const parsePostParams = function parsePostParams(req) {
return new Promise((resolve, reject) => {
let form = new formidable.IncomingForm();
form.parse(req, (err, fields) => {
if (err) {
reject(err);
return;
}
resolve(fields);
});
});
}
//BASE64上传,该方式只能上传小图片,大图片不建议使用这种方式会造成程序卡死,大图片使用form-data上传
router.post('/upload_base64', async (ctx, next) => {
try {
let {
file,
filename
} = await parsePostParams(ctx.req);
file = decodeURIComponent(file);
const suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1];
let spark = new SparkMD5.ArrayBuffer();
file = file.replace(/^data:image\/\w+;base64,/, "");
file = Buffer.from(file, 'base64');
spark.append(file);
let filepath = `${
SERVER_PATH}/${
spark.end()}.${
suffix}`;
await delay();
const isExists = await exists(filepath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filepath, file, false);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: filepath.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (err) {
console.log(err);
ctx.body = {
code: 0,
message: err
}
}
});
const mergeFiles = function mergeFiles(hash, count) {
return new Promise(async (resolve, reject) => {
const dirPath = `${
SERVER_PATH}/${
hash}`;
if (!fs.existsSync(dirPath)) {
reject('还没上传文件,请先上传文件');
return;
}
const filelist = fs.readdirSync(dirPath);
if (filelist.length < count) {
reject('文件还未上传完成,请稍后再试');
return;
}
let suffix;
filelist.sort((a, b) => {
const reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
}).forEach(item => {
!suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;
//将每个文件读取出来并append到以hash命名的新文件中
fs.appendFileSync(`${
SERVER_PATH}/${
hash}.${
suffix}`, fs.readFileSync(`${
dirPath}/${
item}`));
fs.unlinkSync(`${
dirPath}/${
item}`); //删除切片文件
});
await delay(1000); //等待1秒后删除新产生的文件夹
fs.rmdirSync(dirPath);
resolve({
path: `${
HOSTNAME}/upload/${
hash}.${
suffix}`,
filename: `${
hash}.${
suffix}`
})
});
}
router.post('/upload_chunk', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file[0]) || {
};
let filename = (fields && fields.filename[0]) || '';
let [, hash] = /^([^_]+)_(\d+)/.exec(filename);
const dirPath = `${
SERVER_PATH}/${
hash}`;
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const filePath = `${
dirPath}/${
filename}`;
const isExists = await exists(filePath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: filePath.replace(__dirname, HOSTNAME)
}
return;
}
await wirteFile(filePath, file, true);
ctx.body = {
code: 0,
message: '文件上传成功',
serverPath: filePath.replace(__dirname, HOSTNAME)
}
} catch (err) {
ctx.body = {
code: 1,
message: err
}
}
});
//合并切片文件
router.post('/upload_merge', async (ctx, next) => {
const {
hash,
count
} = await parsePostParams(ctx.req);
const {
path,
filename
} = await mergeFiles(hash, count);
ctx.body = {
code: 0,
message: '文件上传成功',
path,
filename
}
});
router.get('/uploaded', async (ctx, next) => {
try {
const {
hash
} = ctx.request.query;
const dirPath = `${
SERVER_PATH}/${
hash}`;
const filelist = fs.readdirSync(dirPath);
filelist.sort((a, b) => {
const reg = /_([\d+])/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
ctx.body = {
code: 0,
message: '获取成功',
filelist
}
} catch (err) {
ctx.body = {
code: 0,
message: '获取失败'
}
}
});
app.use(koastatic('./'));
app.use(router.routes());
app.use(router.allowedMethods());