分片上傳

第一步 作MD5檢查,若是服務端已經有相同的MD5值文件,則直接提示上傳成功;若是已經上傳一部分了則直接返回尚未上傳部分的列表。javascript

第二步 提交分片,服務端按照分片文件向文件中按照offset位置寫入。css

@RestController
@RequestMapping(value = "/file")
public class UploadController {

    private Logger logger = LoggerFactory.getLogger(UploadController.class);

//    @Autowired
//    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private StorageServiceImpl storageService;

    /**
     * 秒傳判斷,斷點判斷
     *
     * @return
     */
    @RequestMapping(value = "checkFileMd5", method = RequestMethod.POST)
    @ResponseBody
    public Object checkFileMd5(String md5) throws IOException {
        Map<String, HashMap<String, Object>> mapState = StorageServiceImpl.mapState;
        HashMap<String, Object> hMap = mapState.get(Constants.FILE_UPLOAD_STATUS);
        Object processingObj = null;
        if (hMap != null) {
            processingObj = hMap.get(md5);
        }
        if (processingObj == null) {
            return new ResultVo(ResultStatus.NO_HAVE);
        }
        String processingStr = processingObj.toString();
        boolean processing = Boolean.parseBoolean(processingStr);
//        String value = stringRedisTemplate.opsForValue().get(Constants.FILE_MD5_KEY + md5);
        String value = (String) StorageServiceImpl.mapMD5.get(Constants.FILE_MD5_KEY + md5);
        if (processing) {
            return new ResultVo(ResultStatus.IS_HAVE, value);
        } else {
            File confFile = new File(value);
            byte[] completeList = FileUtils.readFileToByteArray(confFile);
            List<String> missChunkList = new LinkedList<>();
            for (int i = 0; i < completeList.length; i++) {
                if (completeList[i] != Byte.MAX_VALUE) {
                    missChunkList.add(i + "");
                }
            }
            return new ResultVo<>(ResultStatus.ING_HAVE, missChunkList);
        }
    }

    /**
     * 上傳文件
     *
     * @param param
     * @param request
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public ResultJson fileUpload(MultipartFileParam param, HttpServletRequest request) {
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (isMultipart) {
            logger.info("上傳文件start。");
            try {
                String uri = storageService.uploadFileByMappedByteBuffer(param);
                return ResultJson.ok(uri);
            } catch (IOException e) {
                e.printStackTrace();
                logger.error("文件上傳失敗。{}", param.toString());
            }
            logger.info("上傳文件end。");
        }
        return ResultJson.ok("");
    }
}
public class MultipartFileParam {

    // 用戶id
    private String uid;
    //任務ID
    private String id;
    //總分片數量
    private int chunks;
    //當前爲第幾塊分片
    private int chunk;
    //當前分片大小
    private long size = 0L;
    //文件名
    private String name;
    //分片對象
    private MultipartFile file;
    // MD5
    private String md5;

    public String getUid() {
        return uid;
    }

    public void setUid(String uid) {
        this.uid = uid;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public int getChunks() {
        return chunks;
    }

    public void setChunks(int chunks) {
        this.chunks = chunks;
    }

    public int getChunk() {
        return chunk;
    }

    public void setChunk(int chunk) {
        this.chunk = chunk;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public MultipartFile getFile() {
        return file;
    }

    public void setFile(MultipartFile file) {
        this.file = file;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    @Override
    public String toString() {
        return "MultipartFileParam{" +
                "uid='" + uid + '\'' +
                ", id='" + id + '\'' +
                ", chunks=" + chunks +
                ", chunk=" + chunk +
                ", size=" + size +
                ", name='" + name + '\'' +
                ", file=" + file +
                ", md5='" + md5 + '\'' +
                '}';
    }
}
@Service
public class StorageServiceImpl {
    public static final Map<String, HashMap<String, Object>> mapState = new ConcurrentHashMap();
    public static final Map<String, Object> mapMD5 = new ConcurrentHashMap();
    private final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);
    public static String path_ = null;
    // 保存文件的根目錄
    private static Path rootPaht;

    static {
        path_ = FileManageImpl.class.getClassLoader().getResource("").getPath().replace("WEB-INF/classes/", "files/big/");
        File m = new File(path_);
        m.mkdirs();
        path_ = path_.substring(1);
        rootPaht = Paths.get(path_);
    }


    //這個必須與前端設定的值一致
    private long CHUNK_SIZE = 5242880l;

    private String finalDirPath = path_;

    public void deleteAll() {
        logger.info("開發初始化清理數據,start");
        mapState.remove(Constants.FILE_UPLOAD_STATUS);
        mapState.remove(Constants.FILE_MD5_KEY);
        logger.info("開發初始化清理數據,end");
    }

    public void init() {
        try {
            Files.createDirectory(rootPaht);
            mapState.put(Constants.FILE_UPLOAD_STATUS, new HashMap<>());
            mapState.put(Constants.FILE_MD5_KEY, new HashMap<>());
        } catch (FileAlreadyExistsException e) {
            logger.error("文件夾已經存在了,不用再建立。");
        } catch (IOException e) {
            logger.error("初始化root文件夾失敗。", e);
        }
    }

    public String uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException {
        String fileName = param.getName();
        String md5Val = param.getMd5();
        String uploadDirPath = finalDirPath + md5Val;
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(uploadDirPath);
        File tmpFile = new File(uploadDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
        FileChannel fileChannel = tempRaf.getChannel();

        //寫入該分片數據
        long offset = CHUNK_SIZE * param.getChunk();
        byte[] fileData = param.getFile().getBytes();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        mappedByteBuffer.put(fileData);
        // 釋放
        FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();

        boolean isOk = checkAndSetUploadProgress(param, uploadDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
            return "/files/big/" + md5Val + "/" + fileName;
        }
        return null;
    }

    /**
     * 檢查並修改文件上傳進度
     *
     * @param param
     * @param uploadDirPath
     * @return
     * @throws IOException
     */
    private boolean checkAndSetUploadProgress(MultipartFileParam param, String uploadDirPath) throws IOException {
        String fileName = param.getName();
        File confFile = new File(uploadDirPath, fileName + ".conf");
        RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
        accessConfFile.setLength(param.getChunks());
        accessConfFile.seek(param.getChunk());
        accessConfFile.write(Byte.MAX_VALUE);

        byte[] completeList = FileUtils.readFileToByteArray(confFile);
        byte isComplete = Byte.MAX_VALUE;
        for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
            isComplete = (byte) (isComplete & completeList[i]);
            System.out.println("shard-id" + completeList[i]);
        }

        accessConfFile.close();
        if (isComplete == Byte.MAX_VALUE) {
            HashMap<String, Object> md5 = mapState.get(Constants.FILE_UPLOAD_STATUS);
            if (md5 == null) {
                md5 = new HashMap<>();
                mapState.put(Constants.FILE_UPLOAD_STATUS, md5);
            }
            md5.put(param.getMd5(), "true");

            mapMD5.put(Constants.FILE_MD5_KEY + param.getMd5(), "/files/big/" + param.getMd5() + "/" + fileName);
            return true;
        } else {
            HashMap<String, Object> md5 = mapState.get(Constants.FILE_UPLOAD_STATUS);
            if (md5 == null) {
                md5 = new HashMap<>();
                mapState.put(Constants.FILE_UPLOAD_STATUS, md5);
            }
            md5.put(param.getMd5(), "false");
            mapMD5.put(Constants.FILE_MD5_KEY + param.getMd5(), path_ + param.getMd5() + "/" + fileName + ".conf");
            return false;
        }
    }

    /**
     * 文件重命名
     *
     * @param toBeRenamed   將要修更名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public boolean renameFile(File toBeRenamed, String toFileNewName) {
        //檢查要重命名的文件是否存在,是不是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            logger.info("File does not exist: " + toBeRenamed.getName());
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        return toBeRenamed.renameTo(newFile);
    }

}
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultStatus {
    /**
     * 1 開頭爲判斷文件在系統的狀態
     */
    IS_HAVE(100, "文件已存在!"),

    NO_HAVE(101, "該文件沒有上傳過。"),

    ING_HAVE(102, "該文件上傳了一部分。");


    private final int value;

    private final String reasonPhrase;


    ResultStatus(int value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
    }

    public int getValue() {
        return value;
    }

    public String getReasonPhrase() {
        return reasonPhrase;
    }
}
public class ResultVo<T> {

    private ResultStatus status;

    private String msg;

    private T data;

    public ResultVo(ResultStatus status) {
        this(status, status.getReasonPhrase(), null);
    }

    public ResultVo(ResultStatus status, T data) {
        this(status, status.getReasonPhrase(), data);
    }

    public ResultVo(ResultStatus status, String msg, T data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public ResultStatus getStatus() {
        return status;
    }

    public void setStatus(ResultStatus status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "ResultVo{" +
                "status=" + status +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>斷點上傳</title>
    <link rel="stylesheet" type="text/css" href="css/webuploader.css">
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div id="uploader" class="wu-example">
    <!--用來存放文件信息-->
    <div id="thelist" class="uploader-list"></div>
    <div class="btns">
        <div id="picker">選擇大文件</div>
        <button id="ctlBtn" class="btn btn-default">開始上傳</button>
    </div>
</div>
<!--引入JS-->
<script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script type="text/javascript" src="js/webuploader.min.js"></script>
<script>
    var $btn = $('#ctlBtn');
    var $thelist = $('#thelist');
    var chunkSize = 5 * 1024 * 1024;

    // HOOK 這個必需要再uploader實例化前面
    WebUploader.Uploader.register({
        'before-send-file': 'beforeSendFile',
        'before-send': 'beforeSend'
    }, {
        beforeSendFile: function (file) {
            console.log("beforeSendFile");
            // Deferred對象在鉤子回掉函數中常常要用到,用來處理須要等待的異步操做。
            var task = new $.Deferred();
            // 根據文件內容來查詢MD5
            uploader.md5File(file).progress(function (percentage) {   // 及時顯示進度
                console.log('計算md5進度:', percentage);
                getProgressBar(file, percentage, "MD5", "MD5");
            }).then(function (val) { // 完成
                console.log('md5 result:', val);
                file.md5 = val;
                // 模擬用戶id
                // file.uid = new Date().getTime() + "_" + Math.random() * 100;
                file.uid = WebUploader.Base.guid();
                // 進行md5判斷
                $.post("file/checkFileMd5", {uid: file.uid, md5: file.md5},
                        function (data) {
                            console.log(data.status);
                            var status = data.status.value;
                            task.resolve();
                            if (status == 101) {
                                // 文件不存在,那就正常流程
                            } else if (status == 100) {
                                // 忽略上傳過程,直接標識上傳成功;
                                uploader.skipFile(file);
                                file.pass = true;
                            } else if (status == 102) {
                                // 部分已經上傳到服務器了,可是差幾個模塊。
                                file.missChunks = data.data;
                            }
                        });
            });
            return $.when(task);
        },
        beforeSend: function (block) {
            console.log("block")
            var task = new $.Deferred();
            var file = block.file;
            var missChunks = file.missChunks;
            var blockChunk = block.chunk;
            console.log("當前分塊:" + blockChunk);
            console.log("missChunks:" + missChunks);
            if (missChunks !== null && missChunks !== undefined && missChunks !== '') {
                var flag = true;
                for (var i = 0; i < missChunks.length; i++) {
                    if (blockChunk == missChunks[i]) {
                        console.log(file.name + ":" + blockChunk + ":還沒上傳,如今上傳去吧。");
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    task.reject();
                } else {
                    task.resolve();
                }
            } else {
                task.resolve();
            }
            return $.when(task);
        }
    });

    // 實例化
    var uploader = WebUploader.create({
        pick: {
            id: '#picker',
            label: '點擊選擇文件'
        },
        formData: {
            uid: 0,
            md5: '',
            chunkSize: chunkSize
        },
        //dnd: '#dndArea',
        //paste: '#uploader',
        swf: 'js/Uploader.swf',
        chunked: true,
        chunkSize: chunkSize, // 字節 1M分塊
        threads: 3,
        server: 'file/upload',
        auto: false,

        // 禁掉全局的拖拽功能。這樣不會出現圖片拖進頁面的時候,把圖片打開。
        disableGlobalDnd: true,
        fileNumLimit: 1024,
        fileSizeLimit: 1024 * 1024 * 1024,    // 200 M
        fileSingleSizeLimit: 1024 * 1024 * 1024    // 50 M
    });

    // 當有文件被添加進隊列的時候
    uploader.on('fileQueued', function (file) {
        console.log("fileQueued");
        $thelist.append('<div id="' + file.id + '" class="item">' +
                '<h4 class="info">' + file.name + '</h4>' +
                '<p class="state">等待上傳...</p>' +
                '</div>');
    });

    //當某個文件的分塊在發送前觸發,主要用來詢問是否要添加附帶參數,大文件在開起分片上傳的前提下此事件可能會觸發屢次。
    uploader.onUploadBeforeSend = function (obj, data) {
        console.log("onUploadBeforeSend");
        var file = obj.file;
        data.md5 = file.md5 || '';
        data.uid = file.uid;
    };
    // 上傳中
    uploader.on('uploadProgress', function (file, percentage) {
        getProgressBar(file, percentage, "FILE", "上傳進度");
    });
    // 上傳返回結果
    uploader.on('uploadSuccess', function (file) {
        var text = '已上傳';
        if (file.pass) {
            text = "文件妙傳功能,文件已上傳。"
        }
        $('#' + file.id).find('p.state').text(text);
    });
    uploader.on('uploadError', function (file) {
        $('#' + file.id).find('p.state').text('上傳出錯');
    });
    uploader.on('uploadComplete', function (file) {
        // 隱藏進度條
        // fadeOutProgress(file, 'MD5');
        // fadeOutProgress(file, 'FILE');
    });
    // 文件上傳
    $btn.on('click', function () {
        console.log("上傳...");
        uploader.upload();
        console.log("上傳成功");
    });

    /**
     *  生成進度條封裝方法
     * @param file 文件
     * @param percentage 進度值
     * @param id_Prefix id前綴
     * @param titleName 標題名
     */
    function getProgressBar(file, percentage, id_Prefix, titleName) {
        var $li = $('#' + file.id), $percent = $li.find('#' + id_Prefix + '-progress-bar');
        // 避免重複建立
        if (!$percent.length) {
            $percent = $('<div id="' + id_Prefix + '-progress" class="progress progress-striped active">' +
                    '<div id="' + id_Prefix + '-progress-bar" class="progress-bar" role="progressbar" style="width: 0%">' +
                    '</div>' +
                    '</div>'
            ).appendTo($li).find('#' + id_Prefix + '-progress-bar');
        }
        var progressPercentage = percentage * 100 + '%';
        $percent.css('width', progressPercentage);
        $percent.html(titleName + ':' + progressPercentage);
    }

    /**
     * 隱藏進度條
     * @param file 文件對象
     * @param id_Prefix id前綴
     */
    function fadeOutProgress(file, id_Prefix) {
        $('#' + file.id).find('#' + id_Prefix + '-progress').fadeOut();
    }
</script>
</body>
</html>
相關文章
相關標籤/搜索