web上传zip大文件,java服务接收并解压ZIP文件

本文介绍了一种解决大文件上传的方法,通过Java服务接收zip文件并自动解压。首先,讨论了SpringBoot默认的文件上传限制及如何配置以允许大文件上传。接着,讲解了使用zip4j库进行解压操作的便捷性。最后,展示了使用jQuery AJAX进行文件上传的web端代码示例。

目录

一、背景

二、知识点

三、代码实现

1、依赖

2、web 代码

3、 java服务端


一、背景

公司产品小伙伴画了原型后,需要上传到服务器上供开发查看;由于文件数量很多,100M左右差不多要传30分钟,这期间在替换文件导致原型无法正常查看,耽误开发小伙伴时间;随后建议他们将文件压缩为zip压缩包,基本上传也就30秒左右;但是产品小伙伴不会linux命令,也担心他们将服务器弄的“一塌糊涂”,所以就想着做个简单的zip文件上传并自动解压的简单功能。

二、知识点

  • 大文件上传

SpringBoot 项目上传默认单个文件1M,一次上传请求大小10M,上传的文件超过了这个限制就会报错。

如果上传的文件不是特别大,或者在内网上传,那么就直接修改这个配置即可,web端和服务端开发都会比较简单(本文就是采用这个方式)。但是缺点就是上传时间可能比较长,如果设置了超时,会存在上传超时。

application.yml 配置:

spring:
  servlet:
    multipart:
      # 配置 -1 为无限制
      max-file-size: 100MB
      max-request-size: 100MB

如果上传的文件特别大,并且希望快速上传,可以使用百度的 WebUploader JS 插件对文件进行分片上传,服务端顺序写入到文件中。这个功能可以后续写篇博文。

  • zip文件保存

Spring 框架做文件上传,都是封装到 MultipartFile 对象中的。保存到文件最简单的就是使用 MultipartFile.transferTo(file) 方法。

当然也可以自己拿到文件流读取然后写入到zip文件中,注意,一定要用Zip流操作,用普通的流写的zip不可用。

  • 解压zip文件

使用 zip4j 工具包,解压超级简单,2行代码就搞定了。

  • Jquery ajax 上传文件

不使用 form 表单上传,而是使用 ajax 上传,就不用做两个页面了。ajax 上传文件就需要使用内置的 FormData 对象来作为数据体对象,完成文件的上传。

三、代码实现

1、依赖

<!-- spring boot 父包 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
    <relativePath/>
</parent>

<!-- 项目依赖 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>net.lingala.zip4j</groupId>
            <artifactId>zip4j</artifactId>
            <version>2.9.1</version>
        </dependency>
    </dependencies>

application.yml 配置见上方。 

2、web 代码

index.html 代码,省略不重要的标签,节约篇幅

<!-- Bootstrap 和 treeview CSS 文件 -->
<link rel="stylesheet" href="libs/bootstrap.min.css">
<link rel="stylesheet" href="libs/bootstrap-treeview.min.css">
<!-- body -->
<div class="container">
    <h2>原型部署工具</h2>
    <div>说明:1、选择要部署的路径;2、选择部署的zip文件;3、点击部署</div>
    <br>
    <br>
    <div class="row">
        <div class="col-md-8">
            <div>
                <div class="form-group">
                    <label for="tree">部署路径</label>
                    <div id="tree"></div>
                </div>
                <div class="form-group">
                    <label for="file">zip文件</label>
                    <input type="file" id="file" accept=".zip" required>
                    <p class="help-block">仅支持zip文件,且不超过100MB</p>
                </div>
                <button type="button" class="btn btn-default" id="bt" disabled="disabled" onclick="deploy()">提交</button>
            </div>
        </div>
    </div>
</div>
<!-- js 依赖 -->
<script src="libs/jquery-3.6.0.min.js"></script>
<script src="libs/bootstrap.min.js"></script>
<script src="libs/bootstrap-treeview.min.js"></script>
<script src="index.js"></script>

index.js 简略版

var $tree = $('#tree');
var $file = $('#file');
// 用于校验的字段
var deployPath;
var filename;
// 加载可部署的目录树
$.ajax({
    url: "deploy/path/tree",
    type: "get",
    success: function (data) {
        $tree.treeview({
            data: [data],
            onNodeSelected: function (event, data) { // 选择树节点事件
                deployPath = data.path;
                $("button").removeAttr("disabled");
            }
        });
    },
    error: function (err) {
        console.error(err);
        alert("加载部署路径 tree 错误");
    }
});

$file.on("change", function () { // 文件选择事件
    filename = $file.val();
});
// 上传文件
function deploy() {
    // 校验字段省略
    // 使用 ajax 上传文件,需要用 FormData 对象
    var formData = new FormData();
    formData.append("path", deployPath);
    formData.append("file", $file[0].files[0]); // 要上传的文件
    $.ajax({
        url: "deploy/upload",
        type: "post",
        data: formData,
        contentType: false,
        processData: false,
        success: function (data) {
            console.log("部署完成");
        },
        error: function (err) {
            console.error(err);
        }
    });
}

3、 java服务端代码

Controller 代码(省略部分不重要的代码)

package com.chc.yx.deploy.web;

import com.chc.yx.deploy.bean.PathResult;
import com.chc.yx.deploy.bean.UploadFile;
import net.lingala.zip4j.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

/**
 * 部署控制器
 *
 * @author chc
 * @date 2021/12/31
 * @since 1.0
 */
@RestController
@RequestMapping("/deploy")
public class DeployController implements InitializingBean {

    private Logger log = LoggerFactory.getLogger(getClass());
    // 部署基础路径,只能部署在这个目录及其子目录下
    @Value("${deploy.basePath}")
    private String basePath;
    // 解压的编码:解决linux中文乱码
    @Value("${deploy.charset}")
    private String charset;

    /**
     * 读取指定目录下的所有子目录路径名称
     */
    @GetMapping("/path/tree")
    public ResponseEntity<PathResult> pathTree() {
        Path path = Paths.get(basePath);
        // ...省略代码:递归扫描basePath路径下的子目录,以对应的层级封装到PathResult中,供treeview显示可以部署的路径...
        return ResponseEntity.ok(pathResult);
    }

    /**
     * 上传文件
     * @param UploadFile 自定义的参数类
     */
    @PostMapping("/upload")
    public ResponseEntity upload(UploadFile uploadFile) {
        // 部署的路径
        String path = uploadFile.getPath();
        MultipartFile file = uploadFile.getFile();
        // zip文件名称
        String originalFilename = file.getOriginalFilename();
        log.info("部署信息:path = {}, name = {}", path, originalFilename);
        Path zipFilePath = Paths.get(path, originalFilename);
        // ... 省略代码:删除历史同名 zip 文件 ...
        
        try {
            // 使用 MultipartFile.transferTo 保存zip文件
            file.transferTo(zipFilePath);
            log.info("保存{}成功", originalFilename);
        } catch (IOException e) {
            log.error("保存" + zipFilePath + "错误", e);
            return ResponseEntity.status(500).body("保存" + zipFilePath + "错误");
        }

        // 删除非压缩文件
        String targetFilename=originalFilename.substring(0,originalFilename.length()-4);
        Path filePath = Paths.get(path, targetFilename);
        if (Files.exists(filePath)) {
            AtomicBoolean result = new AtomicBoolean(true);
            deleteDirectoryAll(filePath, result);
            log.info("删除{}下的文件结果:{}", targetFilename, result.get());
        }

        // 使用 zip4j 解压:首先指定 zip 文件
        try (ZipFile zipFile = new ZipFile(zipFilePath.toString())) {
            // 指定编码集,主要是解决linux系统上中文乱码
            zipFile.setCharset(Charset.forName(charset));
            // 指定要解压到的目录下,并解压
            zipFile.extractAll(filePath.getParent().toString());
            log.info("解压{}成功", originalFilename);
        } catch (IOException e) {
            log.error("解压zip错误", e);
            return ResponseEntity.status(500).body("解压zip文件错误");
        }

        // 删除zip文件
        try {
            Files.delete(zipFilePath);
        } catch (IOException e) {
            log.info("删除{}成功", originalFilename);
        }
        log.info("部署完成");
        return ResponseEntity.ok("success");
    }

    /**
     * 递归删除目录和下面的文件————java api删除目录的话,目录必须是空的才能删除
     */
    private void deleteDirectoryAll(Path path, final AtomicBoolean result) {
        try (Stream<Path> stream = Files.list(path)) {
            stream.forEach(p -> {
                if (Files.isDirectory(p)) {
                    deleteDirectoryAll(p, result);
                }
                try {
                    Files.delete(p);
                } catch (IOException e) {
                    log.error("删除文件错误", e);
                    result.set(false);
                }
            });
        } catch (IOException e) {
            log.error("list path error", e);
            result.set(false);
        }
    }

}

上面Controller中提供了两个配置参数:

1、deploy.basePath 用于指定可以部署的根目录

2、deploy.charset 用于指定解码的字符集,主要是用于解决 linux 系统上中文文件名的乱码问题的;在linux上如果使用 UTF-8 解码存在中文乱码,则可以使用 CP936 编码。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值