专栏首页Node开发服务端文件上传

服务端文件上传

上一篇谈到了小程序端从选择文件到文件的上传下载整个流程。但是文件上传服务器的真正操作实际上是在服务器实现。本篇文章主要谈谈服务端如何实现文件上传到服务器并返回可支持访问的url。首先,我们可以先考虑下业务逻辑。我给出的方案一是这样一个简单逻辑:将上传文件分成图片上传和文件上传两部分逻辑。为什么要区分两部分逻辑呢?因为我们假设一个业务场景:商品上架功能需要上传商品主图,轮播图等一系列图片,我们如果一次只能上传一张图片,则得调用多次接口,会造成服务器带宽和资源的浪费。所以我们处理图片上传我们可以设置图片数组放置需上传的图片。那对于非图片的文件呢?比如我们要上传一个视频,可能几十M,我们同时上传十个八个,这时候客户端迟迟得不到响应,用户体验会很差,所以我们在处理非图片文件时一般需要一个一个文件进行上传。接下来我们来看下服务端如何实现文件上传。

用过Node的人应该都知道,Node实现文件上传一般都需要使用multiparty库,我们首先需要生成multiparty对象并配置文件最终上传的路径:

//生成multiparty对象,并配置上传目标路径
var form = new multiparty.Form({uploadDir: (mainPath + '/picTemp/')});

我们生成multiparty对象后,就可以使用multiparty.from().parse(req, callback)进行文件上传。文件上传成功实际上就会上传到我们刚才定义的上传目录中,然后返回files。我们可以看下文件上传效果:

这时候有人说文件上传解决了,当然没那么简单。我们文件上传看似解决了,但是还需要考虑各种各样的bug场景,简单举几个例子:服务器设置文件上传最大为25M,我上传一个50M的文件,这时候服务器肯定返回413状态码标识文件太大。再比如我们需要限制文件最大上传数量等等逻辑。所以接下来我们开始慢慢优化这个文件上传功能。首先我们需要先对参数做限制,一个变量名只能对应一个文件,比如我上传两个文件,文件名都用mp4_url,这时候肯定不允许,这时候我们需要报错并删除已上传图片:

//查看图片是否超过限制
        var picNum = 0;
        par.picNames = Object.keys(files);
        var picSizeArr = [];
        for(var picKey in files){
            if(files[picKey].length > 1){//每个名字只能带一张图片
                delPicsWithFiles(files);
                return cb('文件参数有误', 400);
            }

            par[picKey] = files[picKey][0].path;
            picNum += files[picKey].length;
            picSizeArr.push(parseInt(files[picKey][0].size));
        }
        
        //根据上传来的files表单删除图片
        function delPicsWithFiles(files) {
            //图片超过限制,删除上传来的图片
            for (var key in files) {
                files[key].forEach(function (picObj) {
                    var uploadedPath = picObj.path;
                    fs.unlink(uploadedPath, function () {
                    });
                });
            }
        }

第一步校验通过了,下一步就是针对图片和非图片做不同的操作。图片允许多图同时上传,所以我们需要判断上传的图片是否超过我们限制的最大张数,如果图片张数超限,则删除所有已上传图片:

if(picNum > maxPic){
   //图片超过限制,删除上传来的图片
  delPicsWithFiles(files);
  return cb('图片个数超过限制', 400);
}

并且需要判断图片文件大小是否符合规范,一般大小要和服务器配置一致,防止文件大小超过服务器限制大小。

if(picSizeArr[0] > 4000000) {
   delPicsWithFiles(files);
   return cb('图片过大,请重新选图!',400);
}

一般上传功能会有业务逻辑操作,比如上传成功保存数据库。所以我们得对参数进行校验,比如参数不全的情况就得删除所有已上传图片:

//检验参数是否正确,包括图片命名,不正确的话去删除上传的图片,并且返回错误
            checkParFunc(par,function (err,errCode) {

                if(!err){//验证正确,去重命名
                    par.files = files;
                    picHelp.renamePics(par,pathDir,isNeedUid,function (err,errCode,param) {
                        if(err){
                            cb(err, errCode, param);
                            delPicsWithFiles(files);
                            return;
                        }

                        cb(null, 0, param);
                    });

                    return;
                }

                //验证不正确,删除上传来的图片
                delPicsWithFiles(files);
                cb(err,errCode);
            });
            
            function checkParFunc(par, cb) {
              if (!par.banner1 || !par.shopTitle1 || !par.price1 || !par.score1 || !par.linkUrl1) {
                  return cb('参数不全', 400);
              }
          
              cb(null, 0, par);
          }

如果到这里检验通过一般来说我们图片上传业务逻辑没问题了。但是我们还是可以继续优化,刚才上传成功的截图我们可以看到文件上传后文件名都是随机字符串,我们很多时候都是需要对文件上传做分类才可以维护数据。所以下一步我们通过分割时间戳按照时间来将上传的图片转移到新的文件夹存储,并且我们移动到真正存储的文件夹时,通过fs.readFile()取到文件后缀名,然后将文件重命名成按时间戳进行命名,最终移动文件夹返回文件所在的地址,文件上传逻辑大功告成:

//给上传的图片重命名 //par:参数 picType:路径名
picHelp.renamePics = function (par,picType,isNeedUid,cb) {
    if(!par.files){
        cb('参数有误',400);
        return;
    }
    //构造路径
    var uid = 0;
    if(par.userInfo){
        uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid;
    }
    var date = new Date();
    var userPath = '/' + picType;
    userPath += '/' + date.getFullYear();
    userPath += '/' + (date.getMonth()+1);
    userPath += '/' + date.getDate();
    if(isNeedUid == true) {
        userPath += '/' + parseInt(uid / 100);
        userPath += '/' + uid;
    }
    mkdirs((mainPath + userPath),function (err) {//创建目录
        if(err){
            cb(err,400);
            return;
        }

        userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds();
        changeDir(par, 0, userPath, function (err, par) {

            if (err) {
                cb(err, 400,par);
                return;
            }
            cb(null, 0, par);
        });
    });
}

//递归创建目录 异步方法
function mkdirs(dirname, callback) {
    fs.exists(dirname, function (exists) {
        if (exists) {
            callback(null);
        } else {
            mkdirs(path.dirname(dirname), function () {
                fs.mkdir(dirname, callback);
            });
        }
    });
}

//更新图片路径
function changeDir(par,index,userPath,callback) {
    var keyArr = Object.keys(par.files);

    if(keyArr.length < 1){
        callback(null,par);
        return;
    }
    var picObj = par.files[keyArr[index]][0];
    var uploadedPath = picObj.path;

    fs.readFile(uploadedPath, function (err,bytesRead) {
        if (err) {
            callback(err,par);
            return;
        }
        var info = imageInfo(bytesRead);
        var type;
        if(!info || !info.format){
            type = '.jpg';
        }else {
            type = imageInfoFileType(info.format);
            if (!type) {
                callback('上传图片格式有误', par);
                return;
            }
        }
        //参数正确   更换图片路径
        var picPach = userPath + type;
        var dstPath = mainPath + picPach;
        checkDirs(dstPath,function (exits) {
            if(exits){
                picPach = userPath + (keyArr.length + index) + type;
                dstPath = mainPath + picPach;
            }
            //重命名为真实文件名
            fs.rename(uploadedPath,dstPath,function (err) {
                if(err){
                    callback(err,par);
                    return;
                }
                par[picObj.fieldName] = picPach;
                if(index < (keyArr.length-1)){
                    changeDir(par,index+1,userPath,callback);
                }else {
                    callback(null,par);
                }
            });
        });
    });
}

讲完了图片上传功能,那针对非图片上传如何实现呢?实际上非文件上传我们可以设置一次只允许上传一个文件,然后判断文件大小是否超过限制,然后一样验证参数是否又出现参数不全等情况,最后一样进行按时间戳分割移动到当天文件夹下存放并进行重命名成按时间戳命名并返回图片路径。逻辑和刚才图片处理类似所以我们直接看看代码:

if(par.mp4_url) {
   if(!files.mp4_url || !files.mp4_url[0] || !files.mp4_url[0].size || files.mp4_url[0].size == 0) {
     cb('视频上传时发生错误!', 400);
        return;
     }

     if(files.mp4_url[0].size > 8000000) {
        fs.unlink(par.mp4_url, function () {});
        cb('文件文件过大',400);
        return;
     }

     delete files['mp4_url'];
     delPicsWithFiles(files);
     pathDir = 'bbs_mp4';

     checkParFunc(par, function (err,errCode) {
        if(err){
          fs.unlink(par.mp4_url, function () {});
          return cb(err, errCode);
        } 
                
        picHelp.renameVideo(par,pathDir,isNeedUid,function (err,errCode,param) {
           if(err){
             cb(err,errCode,param);
             fs.unlink(par.mp4_url, function () {});
             return;
           }   
           cb(null, 0, param);
        });
        return;
     });
}

//文件上传
picHelp.renameVideo = function (par,picType,isNeedUid,cb) {
    var uid = 0;
    if(par.userInfo){
        uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid;
    }
    var date = new Date();
    var userPath = '/' + picType;
    userPath += '/' + date.getFullYear();
    userPath += '/' + (date.getMonth()+1);
    userPath += '/' + date.getDate();
    if(isNeedUid == true) {
        userPath += '/' + parseInt(uid / 100);
        userPath += '/' + uid;
    }
    mkdirs((mainPath + userPath),function (err) {//创建目录
        if(err){
            cb(err,400);
            return;
        }
        userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds();
        var uploadedPath = par.mp4_url;
        fs.readFile(uploadedPath, function (err) {
            if (err) {
                cb(err, 400);
                return;
            }
            var type = par.mp4_url.split('.')[1];
            var picPach = userPath + '.' + type;
            par.mp4_name = picPach;
            var dstPath = mainPath + picPach;
            checkDirs(dstPath,function (exits) {
                if(exits){
                    picPach = userPath + type;
                    dstPath = mainPath + picPach;
                }
                fs.rename(uploadedPath,dstPath,function (err) {
                    if(err){
                        cb(err,400);
                        return;
                    }
                    par.mp4_url = par.mp4_name;
                    cb(null, 0, par);
                });
            });
        });
    });
}

到这里我们可以测试下不同文件的上传效果可以看到测试多种不同格式最后全部成功上传:

目前博客小程序前后端已开源于码云,欢迎来一个star。源码地址:

https://gitee.com/mqzuimeng_admin/wx_blog.git

本文分享自微信公众号 - 程序猿周先森(zhanyue_org)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • express + multer 文件上传入门

    首先利用express-generator express框架生成器生成我们的项目 这里我们采用ejs模板引擎(因为我只会这个)

    Theone67
  • Python线程、协程探究(二)—— 揭开协程的神秘面纱

    在上一篇中我们主要研究了python的多线程困境,发现多核情况下由于GIL的存在,python的多线程程序无法发挥多线程该有的并行威力。在文章的结尾,我...

    eedalong
  • 5个相见恨晚的Linux命令

    作为一个开发人员,经常要用到终端命令,最让人头疼的是记不住繁琐的参数。用谷哥度娘检索效率低下,通过man命令显示的结果又不易阅读。

    程序员小强
  • 由一次线上故障来理解下TCP三握、四挥; Java堆栈分析到源码的探秘

    该服务主要是提供对外的代理接口,大部分接口都会调用第三方接口,获取数据后做聚合处理后,提供给客户端使用。

    黄泽杰
  • 解析浏览器和nodejs环境下console.log()的区别

    怎么会这样呢?在google和Safari的webkit中,console.log并没有立即拍摄对象快照,相反, 他只是存储了一个指向对象的引用,然后在代码返...

    Theone67
  • 阿里Java二面:了解分布式锁?说说ZooKeeper分布式锁的实现原理

    Zookeeper 是一个开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务、配置维护和命名服务等等。

    用户5546570
  • 0730-5.16.2-如何禁用CDH中的静态资源池

    Cloudera Manager提供了向导式的方式为CDH集群启用静态资源池管理,但取消静态资源池管理却不能在同样的界面直接回退。本文主要介绍如何通过CM禁用静...

    Fayson
  • Jenkins 2.X Script Pipeline语法基础

    对于很多初学者来讲,可能接触的都是Declarative Pipeline,即声明式pipeline语法,这种类似我们在做自动化测试时所接触的关键字驱动模式,只...

    苦叶子
  • 020.掌握Pod-Pod基础使用

    Pod可以由1个或多个容器组合而成,通常对于紧耦合的两个应用,应该组合成一个整体对外提供服务,则应该将这两个打包为一个pod。

    木二
  • LeetCode 652: 寻找重复的子树 Find Duplicate Subtrees

    给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。

    爱写bug

扫码关注云+社区

领取腾讯云代金券