文件上传思路梳理

文件上传思路梳理

最近项目里有很多关于图片上传、视频上传的功能,由于以前也只是会用个大概,总是去其他项目里找现成的代码,当 CV 工程师,完成需求的时候很没有成就感,改起来也很烦躁,就花了些时间,自己梳理了一下整个流程。

下面是项目里的一个关于视频上传的完整流程

html 页面

这里就用修改页面了,功能完善一些,像我当初回显的问题卡了好久,还是找前端大佬来帮忙解决的

html 页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
    <th:block th:include="include :: header('修改视频管理')" />
    <th:block th:include="include :: bootstrap-fileinput-css" />
    <th:block th:include="include :: select2-css" />
    <th:block th:include="include :: bootstrap-select-css" />
</head>
<body class="white-bg">
    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
        <form class="form-horizontal m" id="form-video-edit" th:object="${video}">
            <input name="videoId" th:field="*{videoId}" type="hidden">
            <div class="form-group">    
                <label class="col-sm-3 control-label">统一播放:</label>
                <div class="col-sm-8" style="display: flex;align-items: center">
                    <div class="form-check" style="display: flex;align-items: center">
                        <input style="margin: 0 10px 0 0"  type="radio" onchange="handleClick()" name="unified" id="exampleRadios1" value="1">
                        <label  for="exampleRadios1" style="margin: 0" ></label>
                    </div>
                    <div class="form-check" style="margin-left: 30px;display: flex;align-items: center" >
                        <input style="margin: 0 10px 0 0"  type="radio" onchange="handleClick()" name="unified" id="exampleRadios2" value="0">
                        <label for="exampleRadios2" style="margin: 0" ></label>
                    </div>
                </div>
            </div>
            <div class="form-group" id="village">
                <label class="col-sm-3 control-label">所在小区:</label>
                <div class="col-sm-8">
                    <input name="village" th:field="*{village}" class="form-control" type="text" required>
                </div>
            </div>
            <div class="form-group" id="place">
                <label class="col-sm-3 control-label">小区内地点:</label>
                <div class="col-sm-8">
                    <input name="place" th:field="*{place}" class="form-control" type="text" required>
                </div>
            </div>
            <div class="form-group" id="uuId">
                <label class="col-sm-3 control-label">硬件编码:</label>
                <div class="col-sm-8">
                    <input name="uuId" th:field="*{uuId}" class="form-control" type="text" required>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-8">
                    <input name="videoPath" id="videoPath" th:field="*{videoPath}" class="form-control" type="text" style="display: none">
                </div>
            </div>
            <div class="form-group" >
                <form id="form" action="system/video/upload" method="post" enctype="multipart/form-data">
                    <div class="file-loading">
                        <input id="input-id" name="files" multiple type="file">
                    </div>
                </form>
            </div>
        </form>
    </div>
    <th:block th:include="include :: footer" />
    <th:block th:include="include :: bootstrap-fileinput-js" />
    <th:block th:include="include :: bootstrap-select-js" />
    <th:block th:include="include :: select2-js" />
    <script th:inline="javascript">
        var prefix = ctx + "system/video";
        var collType = [[${video.unified}]]

        //回显视频
        var myList = new Array();   //回显文件数组
        var List = new Array();     //定义一个全局变量去接受文件名和id
        var files = document.getElementById("videoPath").value;		//获取后台传递的多个视频路径字符串
        if (files != null && files != ""){
            var fileList = files.substr(0, files.length - 1).split(";");
            for (var i = 0; i<fileList.length; i++){
            	//myList[i]:获取单个视频播放路径
                myList[i] = `<video controls src="http://${window.location.host}${fileList[i]}" style="width:100%;height:100%;"></video>`
                List.push({ filePath: fileList[i] + ';', KeyID: 'init_'+i })
            }
        }

		回显,根据 collType 确定单选按钮的选中项,控制其他输入框是否展示
        $(function () {
            if(collType==1){
                $('#exampleRadios1').attr('checked',true)
                $('#uuId').hide()
                $('#place').hide()
                $('#village').hide()
            }else{
                $('#exampleRadios2').attr('checked',true)
                $('#uuId').show()
                $('#place').show()
                $('#village').show()
            }
        });

        var videoPath = "";
        $(function () {
            initFileInput("input-id");
        })
        //点击事件,通过单选按钮的值,判断其他输入框是否展示
        function handleClick() {
            var type = $('input[type="radio"][name="unified"]:checked').val()
            if(type==1){
                $('#uuId').hide()
                $('#place').hide()
                $('#village').hide()
            }else{
                $('#uuId').show()
                $('#place').show()
                $('#village').show()
            }
        }
        function initFileInput(ctrlName) {
            var control = $('#' + ctrlName);
            control.fileinput({
                language: 'zh', //设置语言
                uploadUrl: prefix + "/upload", //上传的地址
                allowedFileExtensions: [ "mp4","mpeg","avi","navi","asf","wmv","mov","3gp","rmvb","rm","flv"],//接收的文件后缀
                //uploadExtraData:{"id": 1, "fileName":'123.mp3'},
                uploadAsync: true, //默认异步上传
                showUpload: true, //是否显示上传按钮
                showRemove : true, //显示移除按钮
                showPreview : true, //是否显示预览
                showCaption: false,//是否显示标题
                browseClass: "btn btn-default", //按钮样式
                dropZoneEnabled: true,//是否显示拖拽区域
                maxFileSize: 0,//单位为kb,如果为0表示不限制文件大小
                maxFileCount: 10, //表示允许同时上传的最大文件个数
                initialPreviewAsData: false,
                overwriteInitial: false,
                enctype: 'multipart/form-data',
                validateInitialCount:true,
                previewFileIcon: "<i class='glyphicon glyphicon-king'></i>",
                msgFilesTooMany: "选择上传的文件数量({n}) 超过允许的最大数值{m}!",
                initialPreview: myList, //myList[i]:获取视频播放路径
            }).on('filepreupload', function(event, data, previewId, index) {     //上传中触发事件

            }).on("fileuploaded", function (event, data, previewId, index) {    //文件上传成功触发事件
                if (data.response.code == 0){
                    var filePath = data.response.msg
                    List.push({ filePath: filePath, KeyID: previewId })
                    console.log('文件上传成功!');
                }
            }).on('fileerror', function(event, data, msg) {  //一个文件上传失败触发事件
                console.log('文件上传失败!');
            }).on("filecleared",function(event, data, msg){  //清空文件触发事件
                for (var i = 0; i < List.length; i++) {
                    delete List[i];
                }
            }).on("fileremoved", function (event, data, previewId, index) { //删除单个文件触发事件
                console.log(event,'event', data,'data', previewId,'previewId', index,'index')
                for (var i = 0; i < List.length; i++) {
                    if (List[i].KeyID== previewId || List[i].KeyID== data) {
                        List.splice(i,1);
                    }
                }
                console.log('文件删除!');
            }).on("filesuccessremove",function (event, previewId, extra) { //删除上传成功视频的触发事件
                for (var i = 0; i < List.length; i++) {
                    if (List[i].KeyID== previewId) {
                        List.splice(i,1);
                    }
                }
            //点击删除预览框中的删除按钮前触发( initialPreview中的文件除外,只针对于还未上传的文件 )  //id=文件容器的id  , index=文件容器的index
            }).on("filepreremove",function (event, previewId, extra) {
                console.log(event, previewId, extra)
            })
        }

        $("#form-video-edit").validate({
            onkeyup: false,
            focusCleanup: true,
            rules:{
                uuId:{ //硬件编码进行正则限制
                    isUUName:true
                },
                village:{ //小区字数限制
                    isDeptName:true
                },
                place:{ //地点字数限制
                    areaName:true
                }
            }
        });

        function submitHandler() {
            //将数组中的文件路径拼接赋值给 videoPath ,并通过表单传递到后台
            for (var i = 0; i < List.length; i++) {
                videoPath = videoPath + List[i].filePath;
            }
            document.getElementById("videoPath").value = videoPath;

            if ($.validate.form()) {
                $.operate.save(prefix + "/edit", $('#form-video-edit').serialize());
            }
        }
    </script>
</body>
</html>

这里用的 BootstrapFileInput 图片上传的插件,附上 fileinput 的属性作用以及详细说明
直到最后,还有一个小问题,就是编辑的时候,直接删除回显的视频是没用的,删除事件无法触发,除非我们在文件上传框内添加一条新的视频。这个是确实找了很久也没解决掉,欢迎大佬指点🤞

controller 类

在我们选择了视频并点击上传后,视频会根据 uploadUrl 路径,找到 controller 里的上传方法:

controller 类

	/**
     * 视频上传
     */
    @PostMapping("/upload")
    @ResponseBody
    public String picUpload(@RequestParam("files") MultipartFile[] files, HttpServletRequest request, HttpServletResponse response)
    {
        StringBuffer urlString=new StringBuffer();
        AjaxResult ajaxResult = null;
        try {
            if (files != null && files.length > 0) {
                for (int i = 0; i < files.length; i++) {
                    MultipartFile file = files[i];
                    //获取文件路径用";"拼接,返回前端
                    urlString.append(FileUploadUtils.uploadWithFileName(RuoYiConfig.getVersionAttachPath(), file)+";") ;
                }
            }
            ajaxResult = AjaxResult.success(urlString.toString());
            return ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
        } catch (Exception e) {
            ajaxResult = AjaxResult.error(e.getMessage());
            return ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
        }
    }

我们从这里开始,一步步的看,首先是将上传的视频集 files 遍历。

① 这里的 RuoYiConfig 是一个关于项目相关配置的类,他的 getVersionAttachPath() 方法,其实是我们自定义的一个文件保存的路径名字段 profile 也是可以在项目的 application.yml 配置文件里进行更改的,我的本地环境路径是:D:/ruoyi/uploadPath

配置文件 RuoYiConfig

    /** 上传路径 */
    private static String profile;

	public static String getProfile(){
		return profile;
	}

	/**
     * 获取版本附件上传路径
     * @return
     */
    public static String getVersionAttachPath() {
        return getProfile() + "/versionAttach";
    }

② 这里的 FileUploadUtils 是文件上传的工具类,

工具类 FileUploadUtils

package com.ruoyi.common.utils.file;

import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException;
import com.ruoyi.common.exception.file.FileSizeLimitExceededException;
import com.ruoyi.common.exception.file.InvalidExtensionException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.Md5Utils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.RuoYiConfig;

/**
 * 文件上传工具类
 * 
 * @author ruoyi
 */
public class FileUploadUtils
{
    /**
     * 默认大小 50M
     */
    public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024;

    /**
     * 默认的文件名最大长度 100
     */
    public static final int DEFAULT_FILE_NAME_LENGTH = 100;

    /**
     * 默认上传的地址
     */
    private static String defaultBaseDir = RuoYiConfig.getProfile();

    private static int counter = 0;

    public static void setDefaultBaseDir(String defaultBaseDir)
    {
        FileUploadUtils.defaultBaseDir = defaultBaseDir;
    }

    public static String getDefaultBaseDir()
    {
        return defaultBaseDir;
    }

    /**
     * 根据文件路径上传,文件名不变
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @return 文件名称
     * @throws IOException
     */
    public static final String uploadWithFileName(String baseDir, MultipartFile file) throws IOException
    {
        try
        {
        	//通过基目录、上传文件和文件类型数组,获取文件路径
        	//baseDir 相对应用的基目录  --D:/ruoyi/uploadPath/versionAttach
        	//file 上传的文件
        	//MimeTypeUtils.DYNAMIC_ALLOWED_EXTENSION:自定义的文件类型数组
            return uploadUseFileName(baseDir, file, MimeTypeUtils.DYNAMIC_ALLOWED_EXTENSION);
        }
        catch (Exception e)
        {
            throw new IOException(e.getMessage(), e);
        }
    }

    /**
     * 文件上传,文件名不变
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @param allowedExtension 上传文件类型
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException 比如读写文件出错时
     * @throws InvalidExtensionException 文件校验异常
     */
    public static final String uploadUseFileName(String baseDir, MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException
    {
    	//获取上传的文件名长度
        int fileNamelength = file.getOriginalFilename().length();
        //判断文件名是否超过自定义的默认文件名最大长度
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
        {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }

		//通过上传的文件和文件类型数组,校验文件大小、文件类型是否符合
        assertAllowed(file, allowedExtension);

		// DateUtils 日期路径 即年/月/日  --2018/08/08
		//拼接日期路径和文件名 --2018/08/08/视频.mp4
        String fileName = DateUtils.datePath() + "/" + file.getOriginalFilename();

		//通过基目录和文件名,拼接路径,创建文件
        File desc = getAbsoluteFile(baseDir, fileName);
        //保存文件
        //使用此方法保存必须要绝对路径且文件夹必须已存在,否则报错
        file.transferTo(desc);
        //通过基目录和文件名,获取上传后文件的路径  --/profile/versionAttach/2018/08/08/视频.mp4
        String pathFileName = getPathFileName(baseDir, fileName);
        return pathFileName;
    }

	/**
     * 拼接路径,创建文件
     *
     * @param baseDir 相对应用的基目录
     * @param fileName 日期路径和文件名
     */
    private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
    {
    	//拼接路径  --D:/ruoyi/uploadPath/versionAttach/视频.mp4
        File desc = new File(uploadDir + File.separator + fileName);

		//判断创建文件的时文件所在的 文件夹 是否存在
        if (!desc.getParentFile().exists())
        {
        	//避免文件创建失败(该文件所在的文件夹不存在所以创建它所在的文件目录)
        	//mkdirs()方法 ,如果文件夹已经存在,是不会再次创建的
            desc.getParentFile().mkdirs();
        }
        //判断文件是否存在
        if (!desc.exists())
        {
        	//创建文件
            desc.createNewFile();
        }
        return desc;
    }

	/**
     * 拼接文件上传后的路径
     *
     * @param baseDir 相对应用的基目录
     * @param fileName 日期路径和文件名
     */
    private static final String getPathFileName(String uploadDir, String fileName) throws IOException
    {
    	//获取系统自定义文件路径(D:/ruoyi/uploadPath)的长度  --int dirLastIndex = 20
        int dirLastIndex = RuoYiConfig.getProfile().length() + 1;
        //截取基目录(D:/ruoyi/uploadPath/versionAttach)  --String currentDir = "versionAttach"
        String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
        //Constants.RESOURCE_PREFIX:资源映射路径  --/profile
        String pathFileName = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
        //String pathFileName = "/profile/versionAttach/2018/08/08/视频.mp4"
        return pathFileName;
    }

    /**
     * 文件大小校验
     *
     * @param file 上传的文件
     * @return
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws InvalidExtensionException
     */
    public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, InvalidExtensionException
    {
    	//获取文件的大小
        long size = file.getSize();
        //判断文件大小是否超过自定义默认文件大小
        if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE)
        {
            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
        }

		//获取文件名称  --视频.mp4
        String fileName = file.getOriginalFilename();
        //获取文件后缀名  --mp4
        String extension = getExtension(file);
        //判断文件后缀名,是否在文件类型数组里
        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension))
        {
            if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
                        fileName);
            }
            else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
                        fileName);
            }
            else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
                        fileName);
            }
            else
            {
                throw new InvalidExtensionException(allowedExtension, extension, fileName);
            }
        }

    }

    /**
     * 判断MIME类型是否是允许的MIME类型
     *
     * @param extension
     * @param allowedExtension
     * @return
     */
    public static final boolean isAllowedExtension(String extension, String[] allowedExtension)
    {
    	//判断文件后缀名,是否在文件类型数组里
        for (String str : allowedExtension)
        {
            if (str.equalsIgnoreCase(extension))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取文件名的后缀
     * 
     * @param file 表单文件
     * @return 后缀名
     */
    public static final String getExtension(MultipartFile file)
    {
    	//通过FilenameUtils类,获取文件的后缀名
        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
        if (StringUtils.isEmpty(extension))
        {
            extension = MimeTypeUtils.getExtension(file.getContentType());
        }
        return extension;
    }
}

从 upload 方法跳转过来后,跟着代码一步一步的走,一行一行的添加注释,最后发现其实也没有太难,只是很多方法用的少、见的少,所以写的时候很不自信,很慌。这次浅显的流程梳理,也算是使我受益匪浅了。

附上一些其他要用到的工具类:

媒体类型工具类 MimeTypeUtils

package com.ruoyi.common.utils.file;

/**
 * 媒体类型工具类
 * 
 * @author ruoyi
 */
public class MimeTypeUtils
{
    public static final String IMAGE_PNG = "image/png";

    public static final String IMAGE_JPG = "image/jpg";

    public static final String IMAGE_JPEG = "image/jpeg";

    public static final String IMAGE_BMP = "image/bmp";

    public static final String IMAGE_GIF = "image/gif";
    
    public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" };

    public static final String[] FLASH_EXTENSION = { "swf", "flv" };

    public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
            "asf", "rm", "rmvb" };

    public static final String[] DEFAULT_ALLOWED_EXTENSION = {
            // 图片
            "bmp", "gif", "jpg", "jpeg", "png",
            // word excel powerpoint
            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
            // 压缩文件
            "rar", "zip", "gz", "bz2",
            // pdf
            "pdf" };

    public static final String[] DYNAMIC_ALLOWED_EXTENSION = {
            // 图片
            "bmp", "gif", "jpg", "jpeg", "png",
            //视频
            "mp4","mpeg","avi","navi","asf","wmv","mov","3gp","rmvb","rm","flv"
            };

    public static String getExtension(String prefix)
    {
        switch (prefix)
        {
            case IMAGE_PNG:
                return "png";
            case IMAGE_JPG:
                return "jpg";
            case IMAGE_JPEG:
                return "jpeg";
            case IMAGE_BMP:
                return "bmp";
            case IMAGE_GIF:
                return "gif";
            default:
                return "";
        }
    }
}

时间工具类 DateUtils

    /**
     * 日期路径 即年/月/日 如2018/08/08
     */
    public static final String datePath()
    {
        Date now = new Date();
        return DateFormatUtils.format(now, "yyyy/MM/dd");
    }

通用常量信息 Constants

    /**
     * 资源映射路径 前缀
     */
    public static final String RESOURCE_PREFIX = "/profile";

配置文件 application.yml

# 项目相关配置
ruoyi:
  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath)
  profile: D:/ruoyi/uploadPath

好事定律:每件事最后都会是好事,如果不是好事,说明还没到最后。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值