0
点赞
收藏
分享

微信扫一扫

JS实现文件上传下载功能实例解析.


 

对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。

 本文是基于 springboot + vue 实现的文件上传,本文主要介绍服务端实现文件上传的步骤及代码实现,vue的实现步骤及实现请移步本人的另一篇文章


上传分步:

本人分析上传总共分为:

  • 检查文件是否已上传,如已上传可实现秒传
  • 创建临时文件(._tmp)和上传的配置文件(.conf)
  • 使用RandomAccessFile获取临时文件
  • 调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
  • 获取当前是第几个分块,计算文件的最后偏移量
  • 获取当前文件分块的字节数组,用于获取文件字节长度
  • 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer
  • 将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b)
  • 释放缓冲区
  • 检查文件是否全部完成上传,如上传完成将临时文件名为正式文件名

直接上代码

public class FlieChunkUtils {

 

    /**

     * 分块上传

     * 第一步:获取RandomAccessFile,随机访问文件类的对象

     * 第二步:调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel

     * 第三步:获取当前是第几个分块,计算文件的最后偏移量

     * 第四步:获取当前文件分块的字节数组,用于获取文件字节长度

     * 第五步:使用文件通道FileChannel类的 map()方法创建直接字节缓冲器  MappedByteBuffer

     * 第六步:将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b);

     * 第七步:释放缓冲区

     * 第八步:检查文件是否全部完成上传

     *

     * @param param

     * @return

     * @throws Exception

     */

    public static ApiResult uploadByMappedByteBuffer(MultipartFileParam param) throws Exception {

        if (param.getIdentifier() == null || "".equals(param.getIdentifier())) {

            param.setIdentifier(UUID.randomUUID().toString());

        }

        // 判断是否上传

        if (ObjectUtil.isEmpty(param.getFile())) {

            return checkUploadStatus(param);

        }

        // 文件名称

        String fileName = getFileName(param);

        // 临时文件名称

        String tempFileName = param.getIdentifier() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";

        // 获取文件路径

        String filePath = getUploadPath(param);

        // 创建文件夹

        FileUploadUtils.getAbsoluteFile(filePath, fileName);

        // 创建临时文件

        File tempFile = new File(filePath, tempFileName);

        //第一步 获取RandomAccessFile,随机访问文件类的对象

        RandomAccessFile raf = RandomAccessFileUitls.getModelRW(tempFile);

        //第二步 调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel

        FileChannel fileChannel = raf.getChannel();

        //第三步 获取当前是第几个分块,计算文件的最后偏移量

        long offset = (param.getChunkNumber() - 1) * param.getChunkSize();

        //第四步 获取当前文件分块的字节数组,用于获取文件字节长度

        byte[] fileData = param.getFile().getBytes();

        //第五步 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器  MappedByteBuffer

        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);

        //第六步 将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b)

        mappedByteBuffer.put(fileData);

        //第七步 释放缓冲区

        freeMappedByteBuffer(mappedByteBuffer);

        fileChannel.close();

        raf.close();

        //第八步 检查文件是否全部完成上传

        ApiResult result = ApiResult.success();

        boolean isComplete = checkUploadStatus(param, fileName, filePath);

        if (isComplete) {

            // 完成后,临时文件名为正式文件名

            renameFile(tempFile, fileName);

            result.put("endUpload", true);

        }

 

        result.put("filePath", FileUploadUtils.getPathFileName(filePath, fileName));

        result.put("fileName", param.getFile().getOriginalFilename());

        return result;

    }

 

    /**

     * 检查文件是否上传

     *

     * @param param

     * @return

     * @throws Exception

     */

    public static ApiResult checkUploadStatus(MultipartFileParam param) throws Exception {

        String fileName = getFileName(param);

        // 校验conf文件

        File confFile = checkConfFile(fileName, getUploadPath(param));

        // 获取完成列表

        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);

        Listuploadeds = new ArrayList<>();

        for (int i = 0; i < completeStatusList.length; i++) {

            if (completeStatusList[i] == Byte.MAX_VALUE) {

                uploadeds.add(i + 1 + "");

            }

        }

        ApiResultsuccess = ApiResult.success();

        success.put("uploaded", uploadeds);

        success.put("skipUpload", completeStatusList.length > 0 && completeStatusList.length == uploadeds.size());

        // 新文件

        if (ObjectUtil.isEmpty(completeStatusList)) {

            success.put("chunk", false);

            return success;

        }

        if (completeStatusList.length < param.getChunkNumber()) {

            success.put("chunk", false);

            return success;

        }

        byte b = completeStatusList[param.getChunkNumber() - 1];

        if (b != Byte.MAX_VALUE) {

            success.put("chunk", false);

            return success;

        }

        success.put("filePath", FileUploadUtils.getPathFileName(getUploadPath(param), fileName));

        success.put("chunk", true);

        return success;

    }

 

    /**

     * 文件下载

     *

     * @param filePath 文件地址

     * @param request

     * @param response

     * @throws IOException

     */

    public static void download(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException {

        // 初始化 response

        response.reset();

        // 获取文件

        File file = new File(getDownloadPath(filePath));

        long fileLength = file.length();

        //获取从那个字节开始读取文件

        String rangeString = request.getHeader("Range");

        long range = 0;

        if (StrUtil.isNotBlank(rangeString)) {

            range = Long.valueOf(rangeString.substring(rangeString.indexOf("=") + 1, rangeString.indexOf("-")));

        }

        if (range >= fileLength) {

            throw new CustomException("文件读取长度过长");

        }

        long byteLength = 1024 * 1024;

        if (range + byteLength > fileLength) {

            byteLength = fileLength;

        }

        // 随机读文件RandomAccessFile

        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");

        try {

            // 移动访问指针到指定位置

            randomAccessFile.seek(range);

            // 每次请求只返回1MB的视频流

            byte[] bytes = new byte[(int) byteLength];

            int len = randomAccessFile.read(bytes);

            //获取响应的输出流

            OutputStream outputStream = response.getOutputStream();

            //返回码需要为206,代表只处理了部分请求,响应了部分数据

            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

            //设置此次相应返回的数据长度

            response.setContentLength(len);

            //设置此次相应返回的数据范围

            response.setHeader("Content-Range", "bytes " + range + "-" + len + "/" + fileLength);

            // 将这1MB的视频流响应给客户端

            outputStream.write(bytes, 0, len);

            outputStream.close();

            //randomAccessFile.close();

            System.out.println("返回数据区间:【" + range + "-" + (range + len) + "】");

        } finally {

            randomAccessFile.close();

        }

    }

 

    /**

     * 文件重命名

     *

     * @param toBeRenamed   将要修改名字的文件

     * @param toFileNewName 新的名字

     * @return

     */

    private static boolean renameFile(File toBeRenamed, String toFileNewName) {

        //检查要重命名的文件是否存在,是否是文件

        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {

            return false;

        }

        String p = toBeRenamed.getParent();

        File newFile = new File(p + File.separatorChar + toFileNewName);

        //修改文件名

        return toBeRenamed.renameTo(newFile);

    }

 

    /**

     * 检查文件上传进度

     *

     * @return

     */

    private static boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws Exception {

        // 校验conf文件

        File confFile = checkConfFile(fileName, filePath);

        // 读取conf

        RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");

        //设置文件长度

        if (confAccessFile.length() != param.getTotalChunks()) {

            confAccessFile.setLength(param.getTotalChunks());

        }

        //设置起始偏移量

        confAccessFile.seek(param.getChunkNumber() - 1);

        //将指定的一个字节写入文件中 127,

        confAccessFile.write(Byte.MAX_VALUE);

        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);

        byte isComplete = Byte.MAX_VALUE;

        //这一段逻辑有点复杂,看的时候思考了好久,创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127

        for (int i = 0; i < completeStatusList.length && isComplete == Byte.MAX_VALUE; i++) {

            // 按位与运算,将&两边的数转为二进制进行比较,有一个为0结果为0,全为1结果为1  eg.3&5  即 0000 0011 & 0000 0101 = 0000 0001   因此,3&5的值得1。

            isComplete = (byte) (isComplete & completeStatusList[i]);

        }

        if (isComplete == Byte.MAX_VALUE) {

            //如果全部文件上传完成,删除conf文件

            // FileUtils.deleteFile(confFile.getPath());

            return true;

        }

        return false;

    }

 

 

    /**

     * 在MappedByteBuffer释放后再对它进行读操作的话就会引发jvm crash,在并发情况下很容易发生

     * 正在释放时另一个线程正开始读取,于是crash就发生了。所以为了系统稳定性释放前一般需要检 查是否还有线程在读或写

     *

     * @param mappedByteBuffer

     */

    private static void freeMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) {

        try {

            if (mappedByteBuffer == null) {

                return;

            }

            mappedByteBuffer.force();

            AccessController.doPrivileged(new PrivilegedAction() {

                @Override

                public Object run() {

                    try {

                        Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]);

                        //可以访问private的权限

                        getCleanerMethod.setAccessible(true);

                        //在具有指定参数的 方法对象上调用此 方法对象表示的底层方法

                        sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer,

                                new Object[0]);

                        cleaner.clean();

                    } catch (Exception e) {

                        log.error("clean MappedByteBuffer error!!!", e);

                    }

                    return null;

                }

            });

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

 

    private static String getFileName(MultipartFileParam param) {

        String extension;

        if (ObjectUtil.isNotEmpty(param.getFile())) {

            // return param.getFile().getOriginalFilename();

            String filename = param.getFile().getOriginalFilename();

            extension = filename.substring(filename.lastIndexOf("."));

            //return  FileUploadUtils.extractFilename(param.getFile());

        } else {

            extension = param.getFilename().substring(param.getFilename().lastIndexOf("."));

            //return DateUtils.datePath() + "/" + IdUtil.fastUUID() + extension;

        }

        return param.getIdentifier() + extension;

    }

 

    private static String getUploadPath(MultipartFileParam param) {

        return FileUploadUtils.getDefaultBaseDir() + "/" + param.getObjectType();

    }

 

    private static String getDownloadPath(String filePath) {

        // 本地资源路径

        String localPath = WhspConfig.getProfile();

        // 数据库资源地址

        String loadPath = localPath + StrUtil.subAfter(filePath, Constants.RESOURCE_PREFIX, false);

        return loadPath;

    }

 

    private static File checkConfFile(String fileName, String filePath) throws Exception {

        File confFile = FileUploadUtils.getAbsoluteFile(filePath, fileName + ".conf");

        if (!confFile.exists()) {

            confFile.createNewFile();

        }

        return confFile;

    }

}

到此这篇关于springboot大文件上传、分片上传、断点续传、秒传的实现的文章就介绍到这了

示例下载地址

源代码文档

asp.net源码下载,jsp-springboot源码下载,jsp-eclipse源码下载,jsp-myeclipse源码下载,php源码下载,csharp-winform源码下载,vue-cli源码下载,c++源码下载

详细配置信息及思路

效果展示:

JS实现文件上传下载功能实例解析._分块

JS实现文件上传下载功能实例解析._分块_02

JS实现文件上传下载功能实例解析._上传_03

JS实现文件上传下载功能实例解析._分块_04

JS实现文件上传下载功能实例解析._临时文件_05

 

 


举报

相关推荐

0 条评论