当前位置: 主页 > 财经 > 详情
环球信息:注册java实现文件分片上传并且断点续传

程序猿阿嘴   2023-05-26 23:42:13

提示:以下是本篇文章正文内容,下面案例可供参考

一、简单的分片上传

针对第一个问题,如果文件过大,上传到一半断开了,若重新开始上传的话,会很消耗时间,并且你也并不知道距离上次断开时,已经上传到哪一部分了。因此我们应该先对大文件进行分片处理,防止上面提到的问题。

前端代码:

html复制代码文件上传示例

ps:以上代码使用了html+js完成,请求是使用了xhr来发送请求。其中的地址为自己本地的接口地址。由于平时测试并不需要真正上传大型文件,所以每个分片的大小定义为10KB,以此模拟大文件上传。


(相关资料图)

后端代码:

java复制代码@RestController@RequestMapping("/file")public class FileController {    @Autowired    private ResourceLoader resourceLoader;    @Value("${}")    private String uploadPath;    private Map>chunksMap = new ConcurrentHashMap<>();    @PostMapping("/upload")    public void upload(@RequestParam int currentChunk, @RequestParam int totalChunks,                       @RequestParam MultipartFile chunk,@RequestParam String fileName) throws IOException {        // 将分片保存到临时文件夹中        String chunkName = () + "." + currentChunk;        File chunkFile = new File(uploadPath, chunkName);        (chunkFile);        // 记录分片上传状态        ListchunkList = (fileName);        if (chunkList == null) {            chunkList = new ArrayList<>(totalChunks);            (fileName, chunkList);        }        (chunkFile);    }    @PostMapping("/merge")    public String merge(@RequestParam String fileName) throws IOException {        // 获取所有分片,并按照分片的顺序将它们合并成一个文件        ListchunkList = (fileName);        if (chunkList == null || () == 0) {            throw new RuntimeException("分片不存在");        }        File outputFile = new File(uploadPath, fileName);        try (FileChannel outChannel = new FileOutputStream(outputFile).getChannel()) {            for (int i = 0; i < (); i++) {                try (FileChannel inChannel = new FileInputStream((i)).getChannel()) {                    (0, (), outChannel);                }                (i).delete(); // 删除分片            }        }        (fileName); // 删除记录        // 获取文件的访问URL        Resource resource =         ("file:" + uploadPath + fileName); //由于是本地文件,所以开头是"file",如果是服务器,请改成自己服务器前缀        return ().toString();    }}

ps: 使用一个map记录上传了哪些分片,这里将分片存在了本地的文件夹,等到分片都上传完成后合并并删除分片。用ConcurrentHashMap代替HashMap是因为它在多线程下是安全的。

以上只是一个简单的文件上传代码,但是只要在这上面另做修改就可以解决上面提到的问题。

二、解决问题

1. 怎么避免大量的硬盘读写

上面代码有一个弊端,就是将分片的内容存在了本地的文件夹里。而且在合并的时候判断上传是否完全也是从文件夹读取文件的。对磁盘的大量读写操作不仅速度慢,还会导致服务器崩溃,因此下面代码使用了redis来存储分片信息,避免对磁盘过多读写。(你也可以使用mysql或者其他中间件来存储信息,由于读写尽量不要在mysql,所以我使用了redis)。

2.目标文件过大,如果在上传过程中断开了怎么办

使用redis来存储分片内容,当断开后,文件信息还是存储在redis中,用户再次上传时,检测redis是否有该分片的内容,如果有则跳过。

3. 前端页面上传的文件数据与原文件数据不一致该如何发现

前端在调用上传接口时,先计算文件的校验和,然后将文件和校验和一并传给后端,后端对文件再计算一次校验和,两个校验和进行对比,如果相等,则说明数据一致,如果不一致则报错,让前端重新上传该片段。 js计算校验和代码:

js复制代码    // 计算文件的 SHA-256 校验和    function calculateHash(fileChunk) {        return new Promise((resolve, reject) =>{            const blob = new Blob([fileChunk]);            const reader = new FileReader();            (blob);             = () =>{                const arrayBuffer = ;                const crypto =  || ;                const digest = ("SHA-256", arrayBuffer);                (hash =>{                    const hashArray = (new Uint8Array(hash));                    const hashHex = (b =>(16).padStart(2, "0")).join("");                    resolve(hashHex);                });            };             = () =>{                reject(new Error("Failed to calculate hash"));            };        });    }
java复制代码    public static String calculateHash(byte[] fileChunk) throws Exception {        MessageDigest md = ("SHA-256");        (fileChunk);        byte[] hash = ();        ByteBuffer byteBuffer = (hash);        StringBuilder hexString = new StringBuilder();        while (()) {            (("%02x", ()));        }        return ();    }

注意点:

这里前端和后端计算校验和的算法一定要是一致的,不然得不到相同的结果。 在前端中使用了crypto对文件进行计算,需要引入相关的js。 你可以使用script引入也可以直接下载js
html复制代码

crypto的下载地址 如果github打不开,可能需要使用npm下载了

4. 上传过程中如果断开了应该如何判断哪些分片没有上传

对redis检测哪个分片的下标不存在,若不存在则存入list,最后将list返回给前端

java复制代码boolean allChunksUploaded = true;ListmissingChunkIndexes = new ArrayList<>(); for (int i = 0; i < (); i++) {     if (!((i))) {         allChunksUploaded = false;         (i);     } } if (!allChunksUploaded) {     return (_REQUEST).body(missingChunkIndexes); }

三、完整代码

1、引入依赖

xml复制代码 lettuce-corespring-boot-starter-data-redis

lettuce是一个Redis客户端,你也可以不引入,直接使用redisTemplat就行了

2、前端代码

html复制代码File Upload Demo

3、后端接口代码

java复制代码@RestController@RequestMapping("/file2")public class File2Controller {    private static final String FILE_UPLOAD_PREFIX = "file_upload:";    @Autowired    private ResourceLoader resourceLoader;    @Value("${}")    private String uploadPath;    @Autowired    private ThreadLocalredisConnectionThreadLocal;    //    @Autowired//    private RedisTemplate redisTemplate;    @PostMapping("/upload")    public ResponseEntityuploadFile(@RequestParam("chunk") MultipartFile chunk,                                        @RequestParam("chunkIndex") Integer chunkIndex,                                        @RequestParam("chunkSize") Integer chunkSize,                                        @RequestParam("chunkChecksum") String chunkChecksum,                                        @RequestParam("fileId") String fileId) throws Exception {        if ((fileId) || (fileId)) {            fileId = ().toString();        }        String key = FILE_UPLOAD_PREFIX + fileId;        byte[] chunkBytes = ();        String actualChecksum = calculateHash(chunkBytes);        if (!(actualChecksum)) {            return (_REQUEST).body("Chunk checksum does not match");        }//        if(!().hasKey(key,(chunkIndex))) {//            ().put(key, (chunkIndex), chunkBytes);//        }        RedisConnection connection = ();        Boolean flag = ((), (chunkIndex).getBytes());        if (flag==null || flag == false) {            ((), (chunkIndex).getBytes(), chunkBytes);        }        return (fileId);    }    public static String calculateHash(byte[] fileChunk) throws Exception {        MessageDigest md = ("SHA-256");        (fileChunk);        byte[] hash = ();        ByteBuffer byteBuffer = (hash);        StringBuilder hexString = new StringBuilder();        while (()) {            (("%02x", ()));        }        return ();    }    @PostMapping("/merge")    public ResponseEntitymergeFile(@RequestParam("fileId") String fileId, @RequestParam("fileName") String fileName) throws IOException {        String key = FILE_UPLOAD_PREFIX + fileId;        RedisConnection connection = ();        try {            MapchunkMap = (());//            Map chunkMap = ().entries(key);            if (()) {                return (_FOUND).body("File not found");            }            MaphashMap = new HashMap<>();            for(entry :()){                ((new String(())),());            }            // 检测是否所有分片都上传了            boolean allChunksUploaded = true;            ListmissingChunkIndexes = new ArrayList<>();            for (int i = 0; i < (); i++) {                if (!((i))) {                    allChunksUploaded = false;                    (i);                }            }            if (!allChunksUploaded) {                return (_REQUEST).body(missingChunkIndexes);            }            File outputFile = new File(uploadPath, fileName);            boolean flag = mergeChunks(hashMap, outputFile);            Resource resource = ("file:" + uploadPath + fileName);            if (flag == true) {                (());//                (key);                return ().body(().toString());            } else {                return (555).build();            }        } catch (Exception e) {            ();            return (_SERVER_ERROR).body(());        }    }    private boolean mergeChunks(MapchunkMap, File destFile) {        try (FileOutputStream outputStream = new FileOutputStream(destFile)) {            // 将分片按照顺序合并            for (int i = 0; i < (); i++) {                byte[] chunkBytes = ((i));                (chunkBytes);            }            return true;        } catch (IOException e) {            ();            return false;        }    }}

4、redis配置

java复制代码@Configurationpublic class RedisConfig {    @Value("${}")    private String host;    @Value("${}")    private int port;    @Value("${}")    private String password;    @Bean    public RedisConnectionFactory redisConnectionFactory() {        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();        (host);        (port);        ((password));        return new LettuceConnectionFactory(config);    }    @Bean    public ThreadLocalredisConnectionThreadLocal(RedisConnectionFactory redisConnectionFactory) {        return (() ->());    }}

使用 redisConnectionThreadLocal 是为了避免多次建立连接,很耗时间

总结

以上就是该功能的完整代码。使用代码记得修改uploadPath,避免代码找不到目录路径。在代码最后,可以使用mysql对整个文件计算校验和,将校验和结果和文件名、文件大小、文件类型存入数据库中,在下次大文件上传前先判断是否存在。若存在就不要上传避免占用空间。

作者:NameGo 链接:/post/7222910781921837093

相关资讯