不灭的焱

革命尚未成功,同志仍须努力下载JDK17

作者:Albert.Wen  添加时间:2022-06-12 22:23:38  修改时间:2023-02-01 17:07:00  分类:Java框架/系统  编辑

最近项目上用到文件分片上传,于是找到了百度的一个开源前端控件webuploader。 于是尝试使用。

下载下来后,它提供的服务器端示例代码是php版的,那么Java版的呢?

其实,上传文件都是按照rfc1867标注来的, 只是分段上传需要在前端多做点事情。分段上传原理其实就是在前端使用JavaScript对文件进行分割成不同小块,然后每次ajax请求就post一小块,直到全部收到为止。

但是,为了确保后端能判断文件是否完整的收到,需要得知当前是第几块,一共多少块,每个分段的大小是多少(前后端同学约定好吧),webuploader是有把这些参数传给后端, 但文档里没说明参数名称啊,看了其他后端example,并且自己做了实验才知道。

参数:

  • name:文件名
  • chunks:一共有多少分片
  • chunk:当前分片是第几片file 文件对象

重点来了:

后端接收到这些参数应该怎么处理? 之前看过一个example ,是把每个分片文件都暂时存起来,命名为 文件名.part_1,文件名.part_2,文件名.part_3...文件名part_n,每次都从1到总分片个数(注:这个值就是chunks的值),遍历这些文件是否存在。如果都存在说明全部上传完成了,则再循环一遍,把所有分段都合并到一个文件里。这么做虽然是可以,但是如果文件很大,最后一个分片到达的时候响应可能会很慢,效率低下。

那么应该怎么解决呢?考虑了一会,联想到,以前使用迅雷之类的工具下载,除下载下来的文件以外,还会有一个额外的文件用来存放下载之类的信息。

受到这个启发,我决定这么设计:每当第n个分片到达时(注:n的值其实就是收到的chunk的值),使用Java的随机文件读写类RandomAccessFile,定位到 n*分片大小(注:每个分片大小跟前端约定好的)的位置。

long offset = chunkSize * param.getChunk();
//定位到该分片的偏移量
accessTmpFile.seek(offset);

然后写入分片内容

//写入该分片数据
accessTmpFile.write(param.getFileItem().get());

同时,往一个配置文件,暂且命名为:文件名.conf,设置长度为chunks的值,也就是分片个数。

accessConfFile.setLength(param.getChunks());

然后往第n个位置写入一个 Byte.MAX_VALUE

accessConfFile.seek(param.getChunk());
accessConfFile.write(Byte.MAX_VALUE);

因为写入的单位就是字节,所以我这么操作就相当于在第n个字节里写入全1的状态,然后检查从0到chunks开始每一个字节进行与操作,一旦到第n个字节发现与运算的结果不是全1(Byte.MAX_VALUE),那么就说明这个文件的第n个部分没有传输完成。

如果conf文件0到chunks的位置全部进行与运算的最后结果还是Byte.MAX_VALUE,那么就说明这个文件已经传输完成,该干嘛就干嘛。

//completeList 检查是否全部完成,如果数组里是否全部都是(全部分片都成功上传)
byte[] completeList = FileUtils.readFileToByteArray(confFile);
byte isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete==Byte.MAX_VALUE; i++) {
    //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
    isComplete = (byte)(isComplete & completeList[i]);
    System.out.println(prefix + "check part " + i + " complete?:" + completeList[i]);
}

if (isComplete == Byte.MAX_VALUE) {
    System.out.println(prefix + "upload complete !!");
}

其实还有另一种想法是,前端传来该文件的md5码,然后后端每次接收到都算一次md5码,如果一致则说明上传成功,但是效率应该也不够上面的好,于是没实现。

现在可以开始例子:

在前端webuploader source的examples/image-upload/upload.js 中可以看到

// 实例化
uploader = WebUploader.create({
    pick: {
        id: '#filePicker',
        label: ''
    },
    formData: {
        uid: 123
    },
    dnd: '#dndArea',
    paste: '#uploader',
    swf: '../../dist/Uploader.swf',
    chunked: false,
    chunkSize: 512 * 1024,
    server: '../../server/fileupload.php',
    // runtimeOrder: 'flash',

    // accept: {
    //     title: 'Images',
    //     extensions: 'gif,jpg,jpeg,bmp,png',
    //     mimeTypes: 'image/*'
    // },

    // 
    disableGlobalDnd: true,
    fileNumLimit: 300,
    fileSizeLimit: 200 * 1024 * 1024,    // 200 M
    fileSingleSizeLimit: 50 * 1024 * 1024    // 50 M
});
  • chunked:被设置为false, 改为true就可以分片上传了。
  • chunkSize:这个后端需要用到,所以前后端需要保持一致。
  • server:改成java后端自己定义的上传文件接口的地址,我这里根据后端例子改成了“http://127.0.0.1:8080/file/test-upload2”
////此处已删除一些旧的不可运行的代码, 代码请看分割线下面

 ----------------------------------------2018 分割线--------------------------------------------------

上面的代码太久远了, 从之前那公司项目里扣下来的, 忘记去掉某些不需要的类了` 在此抱歉

新整理的代码放到github上了

https://github.com/ThomasHuang025/webuploader-spring-example

https://gitee.com/wenjianbao/webuploader-spring-example

 

 

参考:

SpringBoot 上传文件(单个、多个文件)

webuploader 获取文件md5_在浏览器进行大文件分片上传(java服务端实现)

记录: 百度webuploader 分片文件上传java服务器端(spring mvc)示例的优化

解决WebUploader断点续传md5混乱导致的文件合并错误问题

 

 




Spring Boot:RandomAccessFile版本

(1) WebUploaderController.java

package com.wanma.framework_web.widget.webUploader;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.wanma.framework_web.constant.UrlConst;
import com.wanma.framework_web.controller.BaseController;
import com.wanma.framework_web.entity.SysFile;
import com.wanma.framework_web.entity.SysUserFile;
import com.wanma.framework_web.helper.StringHelper;
import com.wanma.framework_web.service.ISysFileService;
import com.wanma.framework_web.service.ISysUserFileService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.RandomAccessFile;

/**
 * 上传文件控制器
 */
@Slf4j
@Controller
public class WebUploaderController extends BaseController {
    @Resource
    private ISysFileService iBaseFileService;

    @Resource
    private ISysUserFileService iUserFileService;

    /**
     * 上传 图片/文件
     */
    @ResponseBody
    @RequestMapping(UrlConst.widget_webUploader_uploadFile)
    public String uploadFile(
            Model model,
            @Value("${setting.uploadAccessUrl}") String uploadAccessUrl,        // 上传文件访问URL
            @Value("${setting.uploadRootPath}") String uploadRootPath,          // 上传文件保存路径
            @Value("${setting.tempUploadRootPath}") String tempUploadRootPath,  // 临时上传文件路径
            WebUploaderForm webUploaderForm
    ) {
        try {

            MultipartFile file = webUploaderForm.getFile();
            if (ObjectUtil.isEmpty(file)) {
                return """
                        {"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Please select upload file."}, "id" : "id"}
                        """;
            }

            // 创建根目录
            if (!FileUtil.exist(uploadRootPath)) {
                FileUtil.mkdir(uploadRootPath);
            }
            if (!FileUtil.exist(tempUploadRootPath)) {
                FileUtil.mkdir(tempUploadRootPath);
            }

            // 删除已过期的临时文件夹(有效期:5小时)
            File[] fileNameList = FileUtil.ls(tempUploadRootPath);
            if (ObjectUtil.isNotEmpty(fileNameList)) {
                for (File tempFile : fileNameList) {
                    if (DateUtil.between(DateUtil.date(tempFile.lastModified()), DateUtil.date(), DateUnit.HOUR) > 5) {
                        try {
                            FileUtil.del(tempFile);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

            String curGuid = webUploaderForm.getGuid();
            String fileExt = FileUtil.extName(file.getOriginalFilename());

            // 是否开启了切片功能
            long chunk = NumberUtil.parseLong(webUploaderForm.getChunk());
            long chunks = NumberUtil.parseLong(webUploaderForm.getChunks());

            // 当前上传文件的
            // (1)临时文件夹
            String curTempDirPath = tempUploadRootPath + FileUtil.FILE_SEPARATOR + curGuid;
            if (!FileUtil.exist(curTempDirPath)) {
                FileUtil.mkdir(curTempDirPath);
            }
            // (2)chunk文件
            String curChunkFilePath = curTempDirPath + FileUtil.FILE_SEPARATOR + "upload.chunks";
            // (3)配置文件
            String curConfigFilePath = curTempDirPath + FileUtil.FILE_SEPARATOR + "upload.conf";

            // 【特别注意】
            // 服务端接收到的分片chunk值,其先后顺序并不一定是完全按照从小到大的顺序!!!
            // 如果直接以追加文件流的形式,合并文件,跟原先的文件就有可能不一样了,md5值改变了,即文件被破坏了。
            //log.info("chunk/chunks=" + chunk + "/" + chunks);

            RandomAccessFile accessCurChunkFilePath = new RandomAccessFile(curChunkFilePath, "rw");
            RandomAccessFile accessCurConfigFilePath = new RandomAccessFile(curConfigFilePath, "rw");

            long offset = WebUploaderConfig.chunkSize * chunk;

            // 定位到该分片的偏移量
            accessCurChunkFilePath.seek(offset);
            // 写入该分片数据
            accessCurChunkFilePath.write(file.getBytes());
            // 【注意】用完了要及时关闭文件锁
            accessCurChunkFilePath.close();

            // 把该分段标记为true,表示已完成
            System.out.println(curGuid + ", progress: " + StringHelper.toString(chunk) + "/" + StringHelper.toString(chunks));
            accessCurConfigFilePath.setLength(chunks);
            accessCurConfigFilePath.seek(chunk);
            accessCurConfigFilePath.write(Byte.MAX_VALUE);
            // 【注意】用完了要及时关闭文件锁
            accessCurConfigFilePath.close();

            // completeList检查是否已全部完成
            // 如果数组里全部都是“已完成”,则表示全部分片都成功上传
            byte[] completeList = FileUtils.readFileToByteArray(new File(curConfigFilePath));
            byte isComplete = Byte.MAX_VALUE;
            for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
                // 与运算,如果有部分没有完成,则isComplete != Byte.MAX_VALUE
                isComplete = (byte) (isComplete & completeList[i]);
                // System.out.println(curGuid + ", check part " + i + " complete?:" + completeList[i]);
            }

            if (isComplete == Byte.MAX_VALUE) {
                System.out.println(curGuid + ", upload complete !!!");
                // 是否已存在
                String md5 = SecureUtil.md5(new File(curChunkFilePath));
                SysFile oldFile = this.iBaseFileService.getByMd5(md5);
                String fileUrl;
                int fileId;
                if (ObjectUtil.isEmpty(oldFile)) {
                    // 保存文件
                    String saveName = md5 + "." + fileExt;
                    String savePath = DateUtil.date().toString("yyyy/MM/dd");
                    String uploadDir = uploadRootPath + FileUtil.FILE_SEPARATOR + savePath;

                    FileUtil.mkdir(uploadDir);
                    String targeFilePath = uploadDir + FileUtil.FILE_SEPARATOR + saveName;
                    FileUtil.move(new File(curChunkFilePath), new File(targeFilePath), true);

                    // 保存到数据库
                    SysFile baseFile = new SysFile();
                    baseFile.setName(file.getOriginalFilename());
                    baseFile.setType(file.getContentType());
                    baseFile.setExt(FileUtil.extName(file.getOriginalFilename()));
                    baseFile.setSize(file.getSize());
                    baseFile.setMd5(md5);
                    baseFile.setSavePath(savePath);
                    baseFile.setSaveName(saveName);
                    this.iBaseFileService.save(baseFile);
                    fileUrl = uploadAccessUrl + "/" + savePath + "/" + saveName;
                    fileId = baseFile.getId();
                } else {
                    fileId = oldFile.getId();
                    fileUrl = uploadAccessUrl + "/" + oldFile.getSavePath() + "/" + oldFile.getSaveName();
                    try {
                        // 删除临时文件
                        FileUtil.del(curChunkFilePath);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    log.info("文件已存在, fileId=" + fileId + ", fileName=" + oldFile.getName() + ", 当前文件名=" + file.getOriginalFilename());
                }

                // 插入 用户文件表
                SysUserFile userFile = new SysUserFile();
                userFile.setUserId(this.getLoginUserId(model));
                userFile.setFileId(fileId);
                userFile.setFileName(file.getOriginalFilename());
                userFile.setFileUrl(fileUrl);
                this.iUserFileService.save(userFile);

                return """
                        {"jsonrpc" : "2.0", "result" : {"fileId" : "#fileId#"}, "id" : "id"}
                        """.replace("#fileId#", StringHelper.toString(userFile.getId()));
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return """
                    {"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Upload file error."}, "id" : "id"}
                    """;
        }

        return """
                {"jsonrpc" : "2.0", "result" : {"fileId" : ""}, "id" : "id"}
                """;
    }
}

(2) WebUploaderConfig.java

package com.wanma.framework_web.widget.webUploader;

public interface WebUploaderConfig {
    boolean chunked = true;         // 是否分片
    int chunkSize = 512 * 1024;     //512KB,分片大小
}

(3) WebUploaderForm.java

package com.wanma.framework_web.widget.webUploader;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class WebUploaderForm {
    private MultipartFile file;
    private String guid;
    private String name;
    private String type;
    private String size;
    private String chunk;
    private String chunks;
}