项目地址:https://gitee.com/y_project/RuoYi-Vue

文件上传下载 package com.ruoyi.web.controller.common;

通用下载请求

@RestController
@RequestMapping("/common")
public class CommonController
{
    /**
     * 通用下载请求
     * 
     * @param fileName 文件名称
     * @param delete 是否删除
     */
    @GetMapping("/download")
    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
    {
        try
        {
            if (!FileUtils.checkAllowDownload(fileName))
            {
                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
            }
            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
            String filePath = RuoYiConfig.getDownloadPath() + fileName;

            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            FileUtils.setAttachmentResponseHeader(response, realFileName);
            FileUtils.writeBytes(filePath, response.getOutputStream());
            if (delete)
            {
                FileUtils.deleteFile(filePath);
            }
        }
        catch (Exception e)
        {
            log.error("下载文件失败", e);
        }
    }
}



public class FileUtils
{
    /**
    * 检查文件是否可下载
    * 
    * @param resource 需要下载的文件
    * @return true 正常 false 非法
    */
    public static boolean checkAllowDownload(String resource)
    {
        // 禁止目录上跳级别
        if (StringUtils.contains(resource, ".."))
        {
            return false;
        }

        // 检查允许下载的文件规则
        if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource)))
        {
            return t rue;
        }

        // 不在允许下载的文件规则
        return false;
    }
}


MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION:
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",
            // 视频格式
            "mp4", "avi", "rmvb",
            // pdf
            "pdf" };


public static String getFileType(String fileName)
{
    int separatorIndex = fileName.lastIndexOf(".");
    if (separatorIndex < 0)
    {
        return "";
    }
    return fileName.substring(separatorIndex + 1).toLowerCase();
}

要先通过 文件名称合法性之后 才能下载 这里只是单纯对参数进行检查 获取文件类型的方法也是若依框架自己写的

checkAllowDownload内做了路径安全的判断,且只能是 MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION内的类型才能下载

否则就抛出一个异常 StringUtils.format其实只是简单将占位符 { } 按照顺序替换为参数

/**
* 格式化字符串<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
* 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
* 
* @param strPattern 字符串模板
* @param argArray 参数列表
* @return 结果
*/
public static String format(final String strPattern, final Object... argArray)
{
    if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray))
    {
        return strPattern;
    }
    final int strPatternLength = strPattern.length();

    // 初始化定义好的长度以获得更好的性能
    StringBuilder sbuf = new StringBuilder(strPatternLength + 50);

    int handledPosition = 0;
    int delimIndex;// 占位符所在位置
    for (int argIndex = 0; argIndex < argArray.length; argIndex++)
    {
        delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
        if (delimIndex == -1)
        {
            if (handledPosition == 0)
            {
                return strPattern;
            }
            else
            { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
                sbuf.append(strPattern, handledPosition, strPatternLength);
                return sbuf.toString();
            }
        }
        else
        {
            if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH)
            {
                if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH)
                {
                    // 转义符之前还有一个转义符,占位符依旧有效
                    sbuf.append(strPattern, handledPosition, delimIndex - 1);
                    sbuf.append(Convert.utf8Str(argArray[argIndex]));
                    handledPosition = delimIndex + 2;
                }
                else
                {
                    // 占位符被转义
                    argIndex--;
                    sbuf.append(strPattern, handledPosition, delimIndex - 1);
                    sbuf.append(C_DELIM_START);
                    handledPosition = delimIndex + 1;
                }
            }
            else
            {
                // 正常占位符
                sbuf.append(strPattern, handledPosition, delimIndex);
                sbuf.append(Convert.utf8Str(argArray[argIndex]));
                handledPosition = delimIndex + 2;
            }
        }
    }
    // 加入最后一个占位符后所有的字符
    sbuf.append(strPattern, handledPosition, strPattern.length());

    return sbuf.toString();
}

假若文件类型通过判断 再设置文件返回前端时新的命名和响应头以及写入二进制字符流

realFileName是 系统当前从1970到现在总的毫秒数 加上 字符串中从第一次出现 ‘_’ 的位置一直到最后的截取 这是因为上传的时候会改名成 2022/07/07/若依环境使用手册_20220707211840A002.docx的格式 结果为 165720082723620220707211840A002.docx

filePathRuoYiConfig.getDownloadPath()是从配置文件中 getProfile() + "/download/"

这里不是上传的 /upload/目录了 所以测试下刚上传的文件立即是下载不下来的 得新建一个 /download//upload/复制进去就可以测试下载了

最后判断是否需要删除再对服务器上保存的文件进行删除

文件上传(单个)

@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
    try
    {
        // 上传文件路径
        String filePath = RuoYiConfig.getUploadPath();
        // 上传并返回新文件名称
        String fileName = FileUploadUtils.upload(filePath, file);
        String url = serverConfig.getUrl() + fileName;
        AjaxResult ajax = AjaxResult.success();
        ajax.put("url", url);
        ajax.put("fileName", fileName);
        ajax.put("newFileName", FileUtils.getName(fileName));
        ajax.put("originalFilename", file.getOriginalFilename());
        return ajax;
    }
    catch (Exception e)
    {
        return AjaxResult.error(e.getMessage());
    }
}

和下载一样先通过配置文件拼接出来文件上传路径

/**
* 文件上传
*
* @param baseDir 相对应用的基目录
* @param file 上传的文件
* @param allowedExtension 上传文件类型
* @return 返回上传成功的文件名
* @throws FileSizeLimitExceededException 如果超出最大大小
* @throws FileNameLengthLimitExceededException 文件名太长
* @throws IOException 比如读写文件出错时
* @throws InvalidExtensionException 文件校验异常
*/
public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
        throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
        InvalidExtensionException
{
    int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
    if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
    {
        throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
    }

    assertAllowed(file, allowedExtension);

    String fileName = extractFilename(file);

    String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
    file.transferTo(Paths.get(absPath));
    return getPathFileName(baseDir, fileName);
}

文件上传的具体逻辑是先判断文件名是否超过长度 再对他进行文件大小和类型校验 String[] allowedExtension内存放的是允许的文件后缀

/**
* 文件大小校验
*
* @param file 上传的文件
* @return
* @throws FileSizeLimitExceededException 如果超出最大大小
* @throws InvalidExtensionException
*/
public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
        throws FileSizeLimitExceededException, InvalidExtensionException
{
    long size = file.getSize();
    if (size > DEFAULT_MAX_SIZE)
    {
        throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
    }

    String fileName = file.getOriginalFilename();
    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 if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION)
        {
            throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
                    fileName);
        }
        else
        {
            throw new InvalidExtensionException(allowedExtension, extension, fileName);
        }
    }
}

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

以上检验都没有问题之后再对文件的名称进行重命名

public static final String extractFilename(MultipartFile file)
{
    return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
            FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
}


public static final String datePath()
{
    Date now = new Date();
    return DateFormatUtils.format(now, "yyyy/MM/dd");
}

这里 StringUtils.format就是对 {}进行替换 分别替换为 当前时间、文件原名、序列号、后缀名

最后就是进行文件存储了 第一步先拼接出来文件存储的文件目录 比如 2022/07/07/若依环境使用手册_20220707211840A002.docx他会在 /upload/判断有没有 /2022/、 /07/、 /07三个目录没有就新建 然后存放目标文件

public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
{
    File desc = new File(uploadDir + File.separator + fileName);

    if (!desc.exists())
    {
        if (!desc.getParentFile().exists())
        {
            desc.getParentFile().mkdirs();
        }
    }
    return desc;
}

File.separator是系统文件文件的分割符为了方便,它被表示为一个字符串。在不同系统会有不同的显示。

第二步就是把文件输出到目标地址 file.transferTo(Paths.get(absPath));这里 transferTo的参数 不能是相对目录只能是绝对目录

最后返回给前端的 fileName注释写的是新文件名称 实际上经过为的测试是类似于 /profile/upload/2022/07/07/upupup_20220707182757A001.txt的返回值

url是拼接而成的 结果为: http://127.0.0.1:8080/profile/upload/2022/07/07/若依环境使用手册_20220707195630A001.docx

String url = serverConfig.getUrl() + fileName;

public class ServerConfig
{
    /**
     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
     * 
     * @return 服务地址
     */
    public String getUrl()
    {
        HttpServletRequest request = ServletUtils.getRequest();
        return getDomain(request);
    }

    public static String getDomain(HttpServletRequest request)
    {
        StringBuffer url = request.getRequestURL();
        String contextPath = request.getServletContext().getContextPath();
        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
    }
}


request.getRequestURL() 返回全路径
request.getRequestURI() 返回除去host(域名或者ip)部分的路径
request.getContextPath() 返回工程名部分,如果工程映射为/,此处返回则为空
request.getServletPath() 返回除去host和工程名部分的路径
此处是运行结果
request.getRequestURL():http://127.0.0.1:8080/common/upload
request.getRequestURI():/common/upload
request.getContextPath():空
request.getServletPath():/common/upload
--- https://blog.csdn.net/weixin_44226263/article/details/104796650

批量上传

/**
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{
    try
    {
        // 上传文件路径
        String filePath = RuoYiConfig.getUploadPath();
        List<String> urls = new ArrayList<String>();
        List<String> fileNames = new ArrayList<String>();
        List<String> newFileNames = new ArrayList<String>();
        List<String> originalFilenames = new ArrayList<String>();
        for (MultipartFile file : files)
        {
            // 上传并返回新文件名称
            String fileName = FileUploadUtils.upload(filePath, file);
            String url = serverConfig.getUrl() + fileName;
            urls.add(url);
            fileNames.add(fileName);
            newFileNames.add(FileUtils.getName(fileName));
            originalFilenames.add(file.getOriginalFilename());
        }
        AjaxResult ajax = AjaxResult.success();
        ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
        ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
        ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
        ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
        return ajax;
    }
    catch (Exception e)
    {
        return AjaxResult.error(e.getMessage());
    }
}

批量上传就是用一个for循环一个一个上传最后插入一个列表返回给前端