Java implementa la carga de fragmentos de archivos y la reanudación del punto de interrupción

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

1. Carga multiparte simple

Para la primera pregunta, si el archivo es demasiado grande y se desconecta a la mitad de la carga, llevará mucho tiempo reiniciar la carga y no sabrá qué parte se ha cargado desde la última vez que se desconectó. . Por lo tanto, primero debemos fragmentar archivos grandes para evitar los problemas mencionados anteriormente.

Código de front-end:

<!DOCTYPE html>
<html>
<head>
    <title>文件上传示例</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<form>
    <input type="file" id="fileInput" multiple>
    <button type="button" onclick="upload()" >上传</button>
</form>
<script>
    function upload() {
        var fileInput = document.getElementById('fileInput');
        var fileName = document.getElementById("fileInput").files[0].name;
        var files = fileInput.files;
        var chunkSize = 1024 * 10; // 每个块的大小为10KB
        var totalChunks = Math.ceil(files[0].size / chunkSize); // 文件总块数
        var currentChunk = 0; // 当前块数

        // 分片上传文件
        function uploadChunk() {
            var xhr = new XMLHttpRequest();
            var formData = new FormData();

            // 将当前块数和总块数添加到formData中
            formData.append('currentChunk', currentChunk);
            formData.append('totalChunks', totalChunks);
            formData.append('fileName',fileName);

            // 计算当前块在文件中的偏移量和长度
            var start = currentChunk * chunkSize;
            var end = Math.min(files[0].size, start + chunkSize);
            var chunk = files[0].slice(start, end);

            // 添加当前块到formData中
            formData.append('chunk', chunk);

            // 发送分片到后端
            xhr.open('POST', '/file/upload');
            xhr.send(formData);

            xhr.onload = function() {
                // 更新当前块数
                currentChunk++;

                // 如果还有未上传的块,则继续上传
                if (currentChunk < totalChunks) {
                    uploadChunk();
                } else {
                    // 所有块都上传完毕,进行文件合并
                    mergeChunks(fileName);
                }
            }
        }

        // 合并所有分片
        function mergeChunks() {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "/file/merge", true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        console.log("文件上传完成:", xhr.responseText);
                    } else {
                        console.error(xhr.responseText);
                    }
                }
            };
            xhr.send("fileName=" + fileName);
        }

        // 开始上传
        uploadChunk();
    }
</script>
</body>
</html>
复制代码

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

Código de fondo:

@RestController
@RequestMapping("/file")
public class FileController {
    @Autowired
    private ResourceLoader resourceLoader;

    @Value("${my.config.savePath}")
    private String uploadPath;

    private Map<String, List<File>> chunksMap = new ConcurrentHashMap<>();

    @PostMapping("/upload")
    public void upload(@RequestParam int currentChunk, @RequestParam int totalChunks,
                       @RequestParam MultipartFile chunk,@RequestParam String fileName) throws IOException {

        // 将分片保存到临时文件夹中
        String chunkName = chunk.getOriginalFilename() + "." + currentChunk;
        File chunkFile = new File(uploadPath, chunkName);
        chunk.transferTo(chunkFile);

        // 记录分片上传状态
        List<File> chunkList = chunksMap.get(fileName);
        if (chunkList == null) {
            chunkList = new ArrayList<>(totalChunks);
            chunksMap.put(fileName, chunkList);
        }
        chunkList.add(chunkFile);
    }

    @PostMapping("/merge")
    public String merge(@RequestParam String fileName) throws IOException {

        // 获取所有分片,并按照分片的顺序将它们合并成一个文件
        List<File> chunkList = chunksMap.get(fileName);
        if (chunkList == null || chunkList.size() == 0) {
            throw new RuntimeException("分片不存在");
        }

        File outputFile = new File(uploadPath, fileName);
        try (FileChannel outChannel = new FileOutputStream(outputFile).getChannel()) {
            for (int i = 0; i < chunkList.size(); i++) {
                try (FileChannel inChannel = new FileInputStream(chunkList.get(i)).getChannel()) {
                    inChannel.transferTo(0, inChannel.size(), outChannel);
                }
                chunkList.get(i).delete(); // 删除分片
            }
        }

        chunksMap.remove(fileName); // 删除记录
        // 获取文件的访问URL
        Resource resource = 
        			resourceLoader.getResource("file:" + uploadPath + fileName); //由于是本地文件,所以开头是"file",如果是服务器,请改成自己服务器前缀
        return resource.getURI().toString();
    }
}
复制代码

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

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

Dos, resuelve el problema.

1. Cómo evitar una gran cantidad de lecturas y escrituras en el disco duro

Una desventaja del código anterior es que el contenido del fragmento se almacena en la carpeta local. Y al fusionar, juzgar si la carga está completa también es leer el archivo de la carpeta. Una gran cantidad de operaciones de lectura y escritura en el disco no solo son lentas, sino que también provocan que el servidor se bloquee, por lo que el siguiente código usa redis para almacenar información de fragmentación para evitar lecturas y escrituras excesivas en el disco. (También puede usar mysql u otro middleware para almacenar información, ya que la lectura y la escritura no deberían estar en mysql, así que uso redis).

2. El archivo de destino es demasiado grande, ¿qué debo hacer si se desconecta durante el proceso de carga?

Use redis para almacenar el contenido del fragmento. Después de la desconexión, la información del archivo aún se almacena en redis. Cuando el usuario cargue nuevamente, verifique si redis tiene el contenido del fragmento y omítalo si es así.

3. Cómo descubrir que los datos del archivo cargados en la página principal no son coherentes con los datos del archivo original

Cuando el front-end llama a la interfaz de carga, primero calcula la suma de verificación del archivo y luego envía el archivo y la suma de verificación al backend, y el backend calcula la suma de verificación nuevamente para el archivo, compara las dos sumas de verificación y si son equal , significa que los datos son consistentes. De lo contrario, se informará un error y el front-end volverá a cargar el segmento. código de suma de comprobación de cálculo js:

    // 计算文件的 SHA-256 校验和
    function calculateHash(fileChunk) {
        return new Promise((resolve, reject) => {
            const blob = new Blob([fileChunk]);
            const reader = new FileReader();
            reader.readAsArrayBuffer(blob);
            reader.onload = () => {
                const arrayBuffer = reader.result;
                const crypto = window.crypto || window.msCrypto;
                const digest = crypto.subtle.digest("SHA-256", arrayBuffer);
                digest.then(hash => {
                    const hashArray = Array.from(new Uint8Array(hash));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
                    resolve(hashHex);
                });
            };
            reader.onerror = () => {
                reject(new Error('Failed to calculate hash'));
            };
        });
    }
复制代码
    public static String calculateHash(byte[] fileChunk) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(fileChunk);
        byte[] hash = md.digest();
        ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
        StringBuilder hexString = new StringBuilder();
        while (byteBuffer.hasRemaining()) {
            hexString.append(String.format("%02x", byteBuffer.get()));
        }
        return hexString.toString();
    }
复制代码

注意点:

  1. Aquí, el algoritmo para calcular la suma de verificación del front-end y el back-end debe ser consistente, de lo contrario no se obtendrá el mismo resultado.
  2. En el front-end, se usa crypto para calcular el archivo, y se debe introducir js relacionado. Puede usar la importación de secuencias de comandos o descargar js directamente
<script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
复制代码

La dirección de descarga de crypto Si no se puede abrir github, es posible que deba usar npm para descargar

4. Si se desconecta durante el proceso de carga, ¿cómo juzgar qué fragmentos no se han cargado?

Para redis, detecte qué subíndice de fragmento no existe, si no existe, guárdelo en la lista y, finalmente, devuelva la lista al front-end

	boolean allChunksUploaded = true;
	List<Integer> missingChunkIndexes = new ArrayList<>();
	 for (int i = 0; i < hashMap.size(); i++) {
	     if (!hashMap.containsKey(String.valueOf(i))) {
	         allChunksUploaded = false;
	         missingChunkIndexes.add(i);
	     }
	 }
	 if (!allChunksUploaded) {
	     return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(missingChunkIndexes);
	 }
复制代码

3. Código completo

1. Introducir dependencias

 <dependency>
  	<groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.1.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
复制代码

lettuce es un cliente de Redis, no necesita importarlo, solo use redisTemplat directamente

2. Código de front-end

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Upload Demo</title>
</head>
<body>
<input type="file" id="fileInput" multiple>
<button type="button" onclick="uploadFile()" >上传</button>
<div id="progressBar"></div>
<script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
<script>
    var fileId = "";
    var fileName = null;
    var file;
    const chunkSize = 1024 * 10; // 每个分片的大小10KB

    async function uploadFile() {
        var fileInput = document.getElementById('fileInput');
        file = fileInput.files[0];
        fileName = document.getElementById("fileInput").files[0].name;
        // 分片上传文件
        const chunks = Math.ceil(file.size / chunkSize);
        for (let i = 0; i < chunks; i++) {
            try {
                await uploadChunk(file, i);
            } catch (error) {
                console.error('Failed to upload chunk', i, error);
                // 如果上传失败,则尝试恢复上传
                try {
                    await uploadChunk(file, i);
                } catch (error) {
                    console.error('Failed to resume upload', i, error);
                    return;
                }
            }
        }
        // 合并文件
        try {
            const fileUrl = await mergeFile();
            console.log('File URL:', fileUrl);
        } catch (error) {
            console.error('Failed to merge file', error);
        }
    }
    function uploadChunk(file, chunkIndex) {
        return new Promise((resolve, reject) => {
            let fileTemp = file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize);
            var myPromise = calculateHash(fileTemp);
            myPromise.then(result =>{
                const formData = new FormData();
                formData.append('chunk',fileTemp);
                formData.append('chunkIndex', chunkIndex);
                formData.append('chunkChecksum', result);
                formData.append('chunkSize', chunkSize);
                formData.append('fileId',fileId);
                const xhr = new XMLHttpRequest();
                xhr.open('POST', '/hospital/file2/upload', true);

                xhr.onload = () => {
                    if (xhr.status === 200) {
                        resolve(xhr.response);
                        fileId = xhr.responseText
                    } else {
                        reject(xhr.statusText);
                    }
                };

                xhr.onerror = () => {
                    reject(xhr.statusText);
                };
                xhr.send(formData);
            })
        });
    }

    function mergeFile() {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            const formData = new FormData();
            formData.append('fileId',fileId);
            formData.append('fileName',fileName);
            xhr.open('POST', '/hospital/file2/merge', true);

            xhr.onload = () => {
                if (xhr.status === 200) {
                    resolve(xhr.response);
                } else {
                    reject(xhr.statusText);
                    resume(xhr.response.replace(/\[|]/g,'').split(','));
                }
            };

            xhr.onerror = () => {
                reject(xhr.statusText);

            };

            xhr.send(formData);
        });
    }
    async function resume(list){
        for (let i = 0; i < list.length; i++) {
            try {
                await uploadChunk(file, i);
            } catch (error) {
                console.error('Failed to upload chunk', i, error);
                // 如果上传失败,则尝试恢复上传
                try {
                    await uploadChunk(file, i);
                } catch (error) {
                    console.error('Failed to resume upload', i, error);
                    return;
                }
            }
        }
        // 合并文件
        try {
            const fileUrl = await mergeFile();
            console.log('File URL:', fileUrl);
        } catch (error) {
            console.error('Failed to merge file', error);
        }
    }

    // 计算文件的 SHA-256 校验和
    function calculateHash(fileChunk) {
        return new Promise((resolve, reject) => {
            const blob = new Blob([fileChunk]);
            const reader = new FileReader();
            reader.readAsArrayBuffer(blob);
            reader.onload = () => {
                const arrayBuffer = reader.result;
                const crypto = window.crypto || window.msCrypto;
                const digest = crypto.subtle.digest("SHA-256", arrayBuffer);
                digest.then(hash => {
                    const hashArray = Array.from(new Uint8Array(hash));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
                    resolve(hashHex);
                });
            };
            reader.onerror = () => {
                reject(new Error('Failed to calculate hash'));
            };
        });
    }

</script>
</body>
</html>
复制代码

3. Código de interfaz de back-end

@RestController
@RequestMapping("/file2")
public class File2Controller {

    private static final String FILE_UPLOAD_PREFIX = "file_upload:";

    @Autowired
    private ResourceLoader resourceLoader;

    @Value("${my.config.savePath}")
    private String uploadPath;
    @Autowired
    private ThreadLocal<RedisConnection> redisConnectionThreadLocal;
    
//    @Autowired
//    private RedisTemplate redisTemplate;

    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(@RequestParam("chunk") MultipartFile chunk,
                                        @RequestParam("chunkIndex") Integer chunkIndex,
                                        @RequestParam("chunkSize") Integer chunkSize,
                                        @RequestParam("chunkChecksum") String chunkChecksum,
                                        @RequestParam("fileId") String fileId) throws Exception {
        if (StringUtils.isBlank(fileId) || StringUtils.isEmpty(fileId)) {
            fileId = UUID.randomUUID().toString();
        }
        String key = FILE_UPLOAD_PREFIX + fileId;
        byte[] chunkBytes = chunk.getBytes();
        String actualChecksum = calculateHash(chunkBytes);
        if (!chunkChecksum.equals(actualChecksum)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Chunk checksum does not match");
        }
//        if(!redisTemplate.opsForHash().hasKey(key,String.valueOf(chunkIndex))) {
//            redisTemplate.opsForHash().put(key, String.valueOf(chunkIndex), chunkBytes);
//        }
        RedisConnection connection = redisConnectionThreadLocal.get();

        Boolean flag = connection.hExists(key.getBytes(), String.valueOf(chunkIndex).getBytes());
        if (flag==null || flag == false) {
            connection.hSet(key.getBytes(), String.valueOf(chunkIndex).getBytes(), chunkBytes);
        }

        return ResponseEntity.ok(fileId);

    }

    public static String calculateHash(byte[] fileChunk) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(fileChunk);
        byte[] hash = md.digest();
        ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
        StringBuilder hexString = new StringBuilder();
        while (byteBuffer.hasRemaining()) {
            hexString.append(String.format("%02x", byteBuffer.get()));
        }
        return hexString.toString();
    }

    @PostMapping("/merge")
    public ResponseEntity<?> mergeFile(@RequestParam("fileId") String fileId, @RequestParam("fileName") String fileName) throws IOException {
        String key = FILE_UPLOAD_PREFIX + fileId;
        RedisConnection connection = redisConnectionThreadLocal.get();
        try {
            Map<byte[], byte[]> chunkMap = connection.hGetAll(key.getBytes());
//            Map chunkMap = redisTemplate.opsForHash().entries(key);
            if (chunkMap.isEmpty()) {
                return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found");
            }

            Map<String,byte[]> hashMap = new HashMap<>();
            for(Map.Entry<byte[],byte[]> entry :chunkMap.entrySet()){
                hashMap.put((new String(entry.getKey())),entry.getValue());
            }
            // 检测是否所有分片都上传了
            boolean allChunksUploaded = true;
            List<Integer> missingChunkIndexes = new ArrayList<>();
            for (int i = 0; i < hashMap.size(); i++) {
                if (!hashMap.containsKey(String.valueOf(i))) {
                    allChunksUploaded = false;
                    missingChunkIndexes.add(i);
                }
            }
            if (!allChunksUploaded) {
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(missingChunkIndexes);
            }

            File outputFile = new File(uploadPath, fileName);
            boolean flag = mergeChunks(hashMap, outputFile);
            Resource resource = resourceLoader.getResource("file:" + uploadPath + fileName);


            if (flag == true) {
                connection.del(key.getBytes());
//                redisTemplate.delete(key);
                return ResponseEntity.ok().body(resource.getURI().toString());
            } else {
                return ResponseEntity.status(555).build();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }

    private boolean mergeChunks(Map<String, byte[]> chunkMap, File destFile) {
        try (FileOutputStream outputStream = new FileOutputStream(destFile)) {
            // 将分片按照顺序合并
            for (int i = 0; i < chunkMap.size(); i++) {
                byte[] chunkBytes = chunkMap.get(String.valueOf(i));
                outputStream.write(chunkBytes);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
}
复制代码

4、redis配置

@Configuration
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(host);
        config.setPort(port);
        config.setPassword(RedisPassword.of(password));
        return new LettuceConnectionFactory(config);
    }
    @Bean
    public ThreadLocal<RedisConnection> redisConnectionThreadLocal(RedisConnectionFactory redisConnectionFactory) {
        return ThreadLocal.withInitial(() -> redisConnectionFactory.getConnection());
    }
}

复制代码

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

总结

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

Supongo que te gusta

Origin juejin.im/post/7222910781921837093
Recomendado
Clasificación