<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
原理:前端通過js讀取檔案,並將大檔案按照指定大小拆分成多個分片,並且計算每個分片的MD5值。前端將每個分片分別上傳到後端,後端在接收到檔案之後驗證當前分片的MD5值是否與上傳的MD5一致,待所有分片上傳完成之後後端將多個分片合併成一個大檔案,並校驗該檔案的MD5值是否與上傳時傳入的MD5值一致;
支援檔案分片上傳,查詢當前已經上傳的分片資訊,取消檔案上傳
package com.aimilin.component.system.service.modular.file.controller; import com.aimilin.common.core.pojo.base.param.BaseParam; import com.aimilin.common.core.pojo.response.ResponseData; import com.aimilin.common.log.annotation.BusinessLog; import com.aimilin.common.log.enums.LogOpTypeEnum; import com.aimilin.common.security.annotation.Permission; import com.aimilin.component.system.service.modular.file.param.SysPartFileParam; import com.aimilin.component.system.service.modular.file.result.SysPartFileResult; import com.aimilin.component.system.service.modular.file.service.SysPartFileService; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * 系統大檔案上傳 * * @version V1.0 * @date 2022/5/24 11:22 */ @Slf4j @RestController public class SysPartFileController { @Resource private SysPartFileService sysPartFileService; /** * 上傳大檔案 * */ @Permission @PostMapping("/sysFileInfo/partUpload") public ResponseData<SysPartFileResult> partUpload(@Validated(BaseParam.add.class) SysPartFileParam partFile) { return ResponseData.success(sysPartFileService.partUpload(partFile)); } /** * 獲取檔案上傳狀態 * */ @Permission @GetMapping("/sysFileInfo/partUpload/status") public ResponseData<SysPartFileResult> getPartUploadStatus(@Validated(BaseParam.detail.class) SysPartFileParam partFile) { return ResponseData.success(sysPartFileService.getPartUploadStatus(partFile)); } /** * 獲取檔案上傳狀態 * */ @Permission @GetMapping("/sysFileInfo/partUpload/cancel") @BusinessLog(title = "檔案_上傳大檔案_取消", opType = LogOpTypeEnum.OTHER) public ResponseData<SysPartFileResult> cancelUpload(@Validated(BaseParam.detail.class) SysPartFileParam partFile) { return ResponseData.success(sysPartFileService.cancelUpload(partFile)); } }
如果按照分片方式上傳檔案需要指定當前大檔案的MD5、分片MD5、分片內容、分片大小、當前檔名稱、檔案總大小等資訊;另外對於每個檔案前端都需要生成一個唯一編碼用於確定當前上傳的分片屬於統一檔案。
package com.aimilin.component.system.service.modular.file.param; import java.io.Serializable; import java.util.Objects; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.aimilin.common.core.pojo.base.param.BaseParam; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.web.multipart.MultipartFile; import javax.validation.constraints.NotNull; /** * 大檔案斷點續傳 * * @version V1.0 * @date 2022/5/24 10:52 */ @Getter @Setter @ToString public class SysPartFileParam extends BaseParam implements Serializable { /** * 檔案上傳Id, 前端傳入的值 */ @NotNull(message = "uid不能為空", groups = {BaseParam.detail.class, BaseParam.add.class}) private String uid; /** * 上傳檔名稱 */ private String filename; /** * 當前檔案塊,從1開始 */ @NotNull(message = "partNumber不能為空", groups = {BaseParam.add.class}) private Integer partNumber; /** * 當前分塊Md5 */ @NotNull(message = "partMd5不能為空", groups = {BaseParam.add.class}) private String partMd5; /** * 分塊大小,根據 totalSize 和這個值你就可以計算出總共的塊數。注意最後一塊的大小可能會比這個要大。 */ @NotNull(message = "partSize不能為空", groups = {BaseParam.add.class}) private Long partSize; /** * 總大小 */ @NotNull(message = "totalSize不能為空", groups = {BaseParam.add.class}) private Long totalSize; /** * 檔案標識,MD5指紋 */ @NotNull(message = "fileMd5不能為空", groups = {BaseParam.add.class}) private String fileMd5; /** * 二進位制檔案 */ @NotNull(message = "file不能為空", groups = {BaseParam.add.class}) private MultipartFile file; /** * 總塊數, (int)totalSize / partSize 最後一個模組要大一點; * * @return 結果 */ public Integer getTotalParts() { if (Objects.isNull(totalSize) || Objects.isNull(partSize)) { return 0; } return new Double(Math.ceil(totalSize * 1.0 / partSize)).intValue(); } public String getFilename() { if (StringUtils.isBlank(this.filename) && Objects.isNull(this.file)) { return null; } return StringUtils.isBlank(this.filename) ? this.file.getOriginalFilename() : this.filename; } }
至於程式碼中的 BaseParam 類,只是定義了一些驗證的分組,類似以下程式碼:
/** * 引數校驗分組:分頁 */ public @interface page { } /** * 引數校驗分組:列表 */ public @interface list { } /** * 引數校驗分組:下拉 */ public @interface dropDown { } /** * 引數校驗分組:增加 */ public @interface add { }
也是定義了三個介面,分片上傳、查詢當前已上傳的分片、取消檔案上傳
package com.aimilin.component.system.service.modular.file.service; import com.aimilin.component.system.service.modular.file.param.SysPartFileParam; import com.aimilin.component.system.service.modular.file.result.SysPartFileResult; /** * 塊檔案上傳 * * @version V1.0 * @date 2022/5/24 10:59 */ public interface SysPartFileService { /** * 檔案塊上傳公共字首 */ public static final String PART_FILE_KEY = "PART_FILE"; /** * 檔案塊上傳 * 1. 將上傳檔案按照partSize拆分成多個檔案塊 * 2. 判斷當前檔案塊是否已經上傳 * 3. 未上傳,則上傳當前文字塊 * 4. 已上傳則不處理 * 5. 統計當前文字塊上傳進度資訊 * 6. 判斷所有文字塊是否已經上傳完成,如果上傳完成則觸發檔案合併 */ public SysPartFileResult partUpload(SysPartFileParam partFile); /** * 獲取檔案上傳狀態 * * @param partFile 上傳檔案資訊 * @return 檔案上傳狀態結果 */ public SysPartFileResult getPartUploadStatus(SysPartFileParam partFile); /** * 取消檔案上傳 * * @param partFile 上傳檔案資訊 * @return 檔案上傳狀態結果 */ public SysPartFileResult cancelUpload(SysPartFileParam partFile); }
服務實現類:
package com.aimilin.component.system.service.modular.file.service.impl; import cn.hutool.core.io.FileUtil; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.aimilin.common.base.file.FilePartOperator; import com.aimilin.common.base.file.param.AbortMultipartUploadResult; import com.aimilin.common.base.file.param.CompleteFileUploadPart; import com.aimilin.common.base.file.param.FileUploadPart; import com.aimilin.common.base.file.param.FileUploadPartResult; import com.aimilin.common.cache.RedisService; import com.aimilin.common.core.consts.CommonConstant; import com.aimilin.common.core.context.login.LoginContextHolder; import com.aimilin.common.core.exception.ServiceException; import com.aimilin.component.system.service.modular.file.convert.SysPartFileConvert; import com.aimilin.component.system.service.modular.file.entity.SysFileInfo; import com.aimilin.component.system.service.modular.file.enums.SysFileInfoExceptionEnum; import com.aimilin.component.system.service.modular.file.enums.SysPartFileEnum; import com.aimilin.component.system.service.modular.file.param.SysPartFileParam; import com.aimilin.component.system.service.modular.file.result.SysPartFileCache; import com.aimilin.component.system.service.modular.file.result.SysPartFileCache.FileInfo; import com.aimilin.component.system.service.modular.file.result.SysPartFileCache.SysFilePart; import com.aimilin.component.system.service.modular.file.result.SysPartFileResult; import com.aimilin.component.system.service.modular.file.service.SysFileInfoService; import com.aimilin.component.system.service.modular.file.service.SysPartFileService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import static com.aimilin.component.system.service.config.FileConfig.DEFAULT_BUCKET; /** * 大檔案上傳功能服務實現 * * @version V1.0 * @date 2022/5/24 11:53 */ @Slf4j @Service public class SysPartFileServiceImpl implements SysPartFileService { @Resource private FilePartOperator fileOperator; @Resource private RedisService redisService; @Resource private SysFileInfoService sysFileInfoService; @Resource private RedissonClient redisson; /** * 檔案塊上傳 * 1. 將上傳檔案按照partSize拆分成多個檔案塊 * 2. 判斷當前檔案塊是否已經上傳 * 3. 未上傳,則上傳當前文字塊 * 4. 已上傳則不處理 * 5. 統計當前文字塊上傳進度資訊 * 6. 判斷所有文字塊是否已經上傳完成,如果上傳完成則觸發檔案合併 * * @param partFile 上傳檔案 * @return SysPartFileResult 檔案上傳結果 */ @Override public SysPartFileResult partUpload(SysPartFileParam partFile) { MultipartFile file = partFile.getFile(); log.info("分塊上傳檔案:{}, partNumber:{}/{}, partSize:{}/{}", partFile.getFilename(), partFile.getPartNumber(), partFile.getTotalParts(), file.getSize(), partFile.getPartSize()); SysPartFileResult partUploadStatus = this.getPartUploadStatus(partFile); // 已經上傳該部分則直接返回當前檔案狀態 if (SysPartFileEnum.SUCCESS.getCode().equals(partUploadStatus.getPartState())) { return partUploadStatus; } // 上傳分片檔案 FileUploadPart fileUploadPart = this.getFileUploadPart(partFile); try { FileUploadPartResult uploadPartResult = fileOperator.uploadPart(fileUploadPart); this.setPartUploadStatus(partFile, uploadPartResult); } catch (Exception e) { log.error("檔案分片上傳失敗,請求:{}:{}", partFile, e.getMessage(), e); throw new ServiceException(SysFileInfoExceptionEnum.FILE_OSS_ERROR); } return this.getPartUploadStatus(partFile); } /** * 獲取檔案上傳狀態 * * @param partFile 上傳檔案資訊 * @return 檔案上傳狀態結果 */ @Override public SysPartFileResult getPartUploadStatus(SysPartFileParam partFile) { SysPartFileCache fileCache = redisService.getCacheObject(getPartFileKey(partFile.getUid())); SysPartFileResult result; // 如果沒有上傳過則返回預設值 if (Objects.isNull(fileCache)) { result = SysPartFileConvert.INSTANCE.toSysPartFileResult(partFile); result.setFileState(SysPartFileEnum.NOT_EXISTS.getCode()); result.setPartState(SysPartFileEnum.NOT_EXISTS.getCode()); } else { result = SysPartFileConvert.INSTANCE.toSysPartFileResult(fileCache, fileCache.getFilePart(partFile.getPartNumber())); } return result; } /** * 取消檔案上傳 * * @param partFile 上傳檔案資訊 * @return 檔案上傳狀態結果 */ @Override public SysPartFileResult cancelUpload(SysPartFileParam partFile) { String cacheKey = getPartFileKey(partFile.getUid()); SysPartFileCache fileCache = redisService.getCacheObject(cacheKey); if (Objects.isNull(fileCache)) { throw new ServiceException(SysFileInfoExceptionEnum.NOT_EXISTED_FILE); } SysPartFileCache.FileInfo fileInfo = fileCache.getFileInfo(); fileOperator.abortMultipartUpload(fileInfo.getBucketName(), fileInfo.getObjectName(), fileInfo.getUploadId()); log.info("取消檔案上傳:{}", partFile.getUid()); SysPartFileResult sysPartFileResult = SysPartFileConvert.INSTANCE.toSysPartFileResult(partFile); sysPartFileResult.setFileState(SysPartFileEnum.CANCELED.getCode()); redisService.deleteObject(cacheKey); return sysPartFileResult; } /** * 檔案分片上傳,設定檔案分片資訊 * * @param partFile 分片檔案引數 * @param uploadPartResult 檔案上傳結果資訊 */ private void setPartUploadStatus(SysPartFileParam partFile, FileUploadPartResult uploadPartResult) { String redisKey = getPartFileKey(partFile.getUid()); if (!redisService.hasKey(redisKey)) { throw new ServiceException(SysFileInfoExceptionEnum.FILE_CACHE_ERROR); } RLock lock = redisson.getLock(CommonConstant.getLockKey(redisKey)); try { lock.lock(); SysPartFileCache fileCache = redisService.getCacheObject(redisKey); Set<SysFilePart> filePartList = fileCache.getFilePartList(); if (Objects.isNull(filePartList)) { filePartList = new HashSet<>(); fileCache.setFilePartList(filePartList); } SysFilePart sysFilePart = new SysFilePart(); sysFilePart.setPartNumber(partFile.getPartNumber()); sysFilePart.setPartState(SysPartFileEnum.SUCCESS.getCode()); sysFilePart.setPartMd5(partFile.getPartMd5()); sysFilePart.setPartSize(partFile.getFile().getSize()); sysFilePart.setFileUploadPartResult(uploadPartResult); filePartList.add(sysFilePart); fileCache.setFileState(SysPartFileEnum.UPLOADING.getCode()); // 所有文字塊都已經上傳完成 if (new HashSet<>(fileCache.getUploadedParts()).size() == fileCache.getTotalParts()) { CompleteFileUploadPart completeFileUploadPart = SysPartFileConvert.INSTANCE.toCompleteFileUploadPart(fileCache); fileOperator.completeMultipartUpload(completeFileUploadPart); log.info("檔案合併完成:{},part: {}/{}", partFile.getFilename(), partFile.getPartNumber(), partFile.getTotalParts()); this.saveFileInfo(partFile, fileCache); fileCache.setFileState(SysPartFileEnum.SUCCESS.getCode()); redisService.setCacheObject(redisKey, fileCache, 1L, TimeUnit.DAYS); } else { redisService.setCacheObject(redisKey, fileCache); } } catch (Exception e) { log.error("設定檔案分片上傳狀態異常,{},上傳結果:{}", partFile, uploadPartResult, e); throw new ServiceException(SysFileInfoExceptionEnum.PART_FILE_SET_STATE_ERROR); }finally { lock.unlock(); } } /** * 儲存檔案資訊到 資料庫 * * @param partFile 分片檔案 * @param fileCache 檔案快取物件 */ private void saveFileInfo(SysPartFileParam partFile, SysPartFileCache fileCache) { SysFileInfo sysFileInfo = new SysFileInfo(); sysFileInfo.setId(Objects.isNull(fileCache.getFileId()) ? IdWorker.getId() : fileCache.getFileId()); sysFileInfo.setFileLocation(fileOperator.getFileLocation().getCode()); sysFileInfo.setFileBucket(fileCache.getFileInfo().getBucketName()); sysFileInfo.setFileOriginName(fileCache.getFilename()); sysFileInfo.setFileSuffix(FilenameUtils.getExtension(fileCache.getFileInfo().getObjectName())); sysFileInfo.setFileSizeKb(SysFileUtils.getFileSizeKb(fileCache.getTotalSize())); sysFileInfo.setFileSizeInfo(FileUtil.readableFileSize(fileCache.getTotalSize())); sysFileInfo.setFileObjectName(fileCache.getFileInfo().getObjectName()); boolean save = sysFileInfoService.save(sysFileInfo); log.info("儲存檔案資訊完成:{},結果:{}", partFile.getFilename(), save); } /** * 獲取檔案上傳分片資訊 * * @param partFile 分片檔案引數 * @return 需要上傳的分片檔案資訊 */ private FileUploadPart getFileUploadPart(SysPartFileParam partFile) { try { SysPartFileCache fileCache = redisService.getCacheObject(getPartFileKey(partFile.getUid())); if (Objects.isNull(fileCache)) { fileCache = this.initSysPartFileCache(partFile); } return SysPartFileConvert.INSTANCE.toFileUploadPart(fileCache.getFileInfo(), partFile); } catch (IOException e) { log.error("獲取檔案分片物件異常:{}", e.getMessage(), e); throw new ServiceException(SysFileInfoExceptionEnum.FILE_STREAM_ERROR); } } /** * 初始化檔案快取物件,進入該方法說明快取為空 * @param partFile 分片檔案 */ private SysPartFileCache initSysPartFileCache(SysPartFileParam partFile) { String key = getPartFileKey(partFile.getUid()); RLock lock = redisson.getLock(CommonConstant.getLockKey(key)); try { lock.lock(); SysPartFileCache fileCache = redisService.getCacheObject(key); if(Objects.isNull(fileCache)){ Long fileId = IdWorker.getId(); String objectName = SysFileUtils.getFileObjectName(partFile.getFilename(), fileId); String uploadId = fileOperator.initiateMultipartUpload(DEFAULT_BUCKET, objectName); fileCache = SysPartFileConvert.INSTANCE.toSysPartFileCache(partFile); fileCache.setFileState(SysPartFileEnum.UPLOADING.getCode()); fileCache.setFileInfo(new FileInfo(DEFAULT_BUCKET, objectName, uploadId)); fileCache.setFileId(fileId); redisService.setCacheObject(getPartFileKey(partFile.getUid()), fileCache); } return fileCache; } catch (Exception e) { log.error("檔案快取初始化異常:{}", partFile, e); throw new ServiceException(SysFileInfoExceptionEnum.PART_FILE_INIT_CACHE_ERROR); }finally { lock.unlock(); } } /** * 獲取檔案快取key * * @param fileId 檔案Id * @return %s:%s:%s */ private String getPartFileKey(String fileId) { return String.format("%s:%s:%s", PART_FILE_KEY, LoginContextHolder.me().getSysLoginUserId(), fileId); } }
package com.aimilin.common.base.file; import com.aimilin.common.base.file.param.*; /** * 大檔案分片操作服務類 * * @version V1.0 * @date 2022/5/24 16:56 */ public interface FilePartOperator extends FileOperator { /** * 初始化分片檔案上傳 * * @param bucketName 檔案桶 * @param key 檔案key * @return 本次檔案上傳唯一標識 */ String initiateMultipartUpload(String bucketName, String key); /** * 上傳分片檔案 * * @param fileUploadPart 分片檔案引數 * @return 上傳結果 */ FileUploadPartResult uploadPart(FileUploadPart fileUploadPart); /** * 完成分片上傳 * * @param completeFileUploadPart 請求物件 * @return 結果資訊 */ CompleteFileUploadPartResult completeMultipartUpload(CompleteFileUploadPart completeFileUploadPart); /** * 取消檔案分片上傳 * * @param bucketName 檔案桶 * @param objectName 物件key * @param uploadId 上傳ID * @return */ void abortMultipartUpload(String bucketName, String objectName, String uploadId); } /** * 檔案分片上傳取消 * * @version V1.0 * @date 2022/5/24 20:32 */ public class AbortMultipartUploadResult { } /** * 完成分片上傳 * * @version V1.0 * @date 2022/5/24 20:07 */ @Getter @Setter @ToString public class CompleteFileUploadPart implements Serializable { private String bucketName; private String objectName; private String uploadId; private List<FileUploadPartResult> partETags; } /** * 分片上傳結果 * * @version V1.0 * @date 2022/5/24 20:08 */ @Getter @Setter @ToString public class CompleteFileUploadPartResult implements Serializable { private String bucketName; private String objectName; private String location; private String eTag; } /** * 檔案分片上傳請求引數 * * @version V1.0 * @date 2022/5/24 17:00 */ @Getter @Setter @ToString public class FileUploadPart implements Serializable { /** * 檔案桶 */ private String bucketName; /** * 檔案key */ private String objectName; /** * 檔案上傳ID */ private String uploadId; /** * 分片大小,設定分片大小。除了最後一個分片沒有大小限制,其他的分片最小為100 KB */ private Long partSize; /** * 設定分片號。每一個上傳的分片都有一個分片號,取值範圍是1~10000,如果超出此範圍,OSS將返回InvalidArgument錯誤碼。 */ private Integer partNumber; /** * 分片Md5簽名 */ private String partMd5; /** * 分片檔案內容 */ @JsonIgnore @JSONField(deserialize = false, serialize = false) private InputStream partContent; } /** * 檔案分片上傳結果 * * @version V1.0 * @date 2022/5/24 17:01 */ @Getter @Setter @ToString public class FileUploadPartResult implements Serializable { /** * 分塊編號 */ private Integer partNumber; /** * 當前分片大小 */ private Long partSize; /** * 上傳結果tag */ private String partETag; }
這裡風兩種實現,1:本地檔案上傳,2:oss物件儲存方式分片上傳
/** * 本地檔案上傳操作 * */ @Slf4j public class LocalFileOperator implements FilePartOperator { @Override public FileLocationEnum getFileLocation() { return FileLocationEnum.LOCAL; } private final LocalFileProperties localFileProperties; private String currentSavePath = ""; private Dict localClient; public LocalFileOperator(LocalFileProperties localFileProperties) { this.localFileProperties = localFileProperties; initClient(); } @Override public void initClient() { if (SystemUtil.getOsInfo().isWindows()) { String savePathWindows = localFileProperties.getLocalFileSavePathWin(); if (!FileUtil.exist(savePathWindows)) { FileUtil.mkdir(savePathWindows); } currentSavePath = savePathWindows; } else { String savePathLinux = localFileProperties.getLocalFileSavePathLinux(); if (!FileUtil.exist(savePathLinux)) { FileUtil.mkdir(savePathLinux); } currentSavePath = savePathLinux; } localClient = Dict.create(); localClient.put("currentSavePath", currentSavePath); localClient.put("localFileProperties", localFileProperties); } @Override public void destroyClient() { // empty } @Override public Object getClient() { // empty return localClient; } @Override public boolean doesBucketExist(String bucketName) { String absolutePath = currentSavePath + File.separator + bucketName; return FileUtil.exist(absolutePath); } @Override public void setBucketAcl(String bucketName, BucketAuthEnum bucketAuthEnum) { // empty } @Override public boolean isExistingFile(String bucketName, String key) { return FileUtil.exist(this.getAbsolutePath(bucketName, key)); } @Override public void storageFile(String bucketName, String key, byte[] bytes) { // 判斷bucket存在不存在 String bucketPath = currentSavePath + File.separator + bucketName; if (!FileUtil.exist(bucketPath)) { FileUtil.mkdir(bucketPath); } // 儲存檔案 FileUtil.writeBytes(bytes, this.getAbsolutePath(bucketName, key)); } @Override public void storageFile(String bucketName, String key, InputStream inputStream, long fileSize) { // 判斷bucket存在不存在 String bucketPath = currentSavePath + File.separator + bucketName; if (!FileUtil.exist(bucketPath)) { FileUtil.mkdir(bucketPath); } // 儲存檔案 FileUtil.writeFromStream(inputStream, this.getAbsolutePath(bucketName, key)); } @Override public byte[] getFileBytes(String bucketName, String key) { // 判斷檔案存在不存在 String absoluteFile = this.getAbsolutePath(bucketName, key); if (!FileUtil.exist(absoluteFile)) { String message = StrUtil.format("檔案不存在,bucket={},key={}", bucketName, key); throw new FileServiceException(message); } else { return FileUtil.readBytes(absoluteFile); } } @Override public void setFileAcl(String bucketName, String key, BucketAuthEnum bucketAuthEnum) { // empty } @Override public void copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) { // 判斷檔案存在不存在 String originFile = this.getAbsolutePath(originBucketName, originFileKey); if (!FileUtil.exist(originFile)) { String message = StrUtil.format("原始檔不存在,bucket={},key={}", originBucketName, originFileKey); throw new FileServiceException(message); } else { // 拷貝檔案 String destFile = this.getAbsolutePath(newBucketName, newFileKey); FileUtil.copy(originFile, destFile, true); } } @Override public String getFileAuthUrl(String bucketName, String key, Long timeoutMillis) { // empty return null; } @Override public void deleteFile(String bucketName, String key) { // 判斷檔案存在不存在 String file = this.getAbsolutePath(bucketName, key); if (!FileUtil.exist(file)) { return; } // 刪除檔案 FileUtil.del(file); } /** * 初始化分片檔案上傳 * * @param bucketName 檔案桶 * @param key 檔案key * @return 本次檔案上傳唯一標識 */ @Override public String initiateMultipartUpload(String bucketName, String key) { return FileNameUtil.getName(key); } /** * 上傳分片檔案 * * @param fileUploadPart 分片檔案引數 * @return 上傳結果 */ @Override public FileUploadPartResult uploadPart(FileUploadPart fileUploadPart) { String partName = fileUploadPart.getObjectName() + "." + fileUploadPart.getPartNumber(); this.storageFile(fileUploadPart.getBucketName(), partName, fileUploadPart.getPartContent(), fileUploadPart.getPartSize()); FileUploadPartResult result = new FileUploadPartResult(); result.setPartNumber(fileUploadPart.getPartNumber()); result.setPartSize(fileUploadPart.getPartSize()); result.setPartETag(partName); // TODO 正常檔案上傳完成之後需要驗證檔案的分片的MD5值是否與前端傳入的值一樣 return result; } /** * 完成分片上傳 * * @param completeFileUploadPart 請求物件 * @return 結果資訊 */ @Override public CompleteFileUploadPartResult completeMultipartUpload(CompleteFileUploadPart completeFileUploadPart) { try { List<FileUploadPartResult> partETags = completeFileUploadPart.getPartETags(); String path = this.getAbsolutePath(completeFileUploadPart.getBucketName(), completeFileUploadPart.getObjectName()); partETags.sort((o1, o2) -> { String p1 = FileNameUtil.extName(o1.getPartETag()); String p2 = FileNameUtil.extName(o2.getPartETag()); return Integer.valueOf(p1).compareTo(Integer.valueOf(p2)); }); Files.createFile(Paths.get(path)); partETags.forEach(c -> { try { Path partPath = Paths.get(this.getAbsolutePath(completeFileUploadPart.getBucketName(), c.getPartETag())); Files.write(Paths.get(path), Files.readAllBytes(partPath), StandardOpenOption.APPEND); Files.delete(partPath); } catch (IOException e) { log.error("合併檔案失敗:{}", e.getMessage(), e); throw new FileServiceException(e.getMessage()); } }); // 檔案合併完成之後需要校驗檔案的MD5值是否與前端傳入的一致 return new CompleteFileUploadPartResult(); } catch (IOException e) { log.error("合併檔案失敗:{}", e.getMessage(), e); throw new FileServiceException(e.getMessage()); } } /** * 取消檔案分片上傳 * * @param bucketName 檔案桶 * @param objectName 物件key * @param uploadId 上傳ID * @return */ @Override public void abortMultipartUpload(String bucketName, String objectName, String uploadId) { try { Path folder = Paths.get(this.getAbsolutePath(bucketName, objectName)).getParent(); String partName = objectName + "."; Files.list(folder) .filter(path -> StrUtil.contains(path.toString(), partName)) .forEach(path -> { try { Files.delete(path); } catch (IOException e) { log.warn("刪除分片檔案失敗:{}", path); } }); } catch (IOException e) { log.error("取消檔案分片上傳異常:{}", e.getMessage(), e); throw new FileServiceException(e.getMessage()); } } /** * 獲取檔案絕對路徑 * * @param bucketName 檔案桶 * @param key 物件key * @return */ private String getAbsolutePath(String bucketName, String key) { return currentSavePath + File.separator + bucketName + File.separator + key; } }
/** * 阿里雲檔案操作 * */ @Slf4j public class AliyunFileOperator implements FilePartOperator { @Override public FileLocationEnum getFileLocation() { return FileLocationEnum.ALIYUN; } /** * 阿里雲檔案操作使用者端 */ private OSS ossClient; /** * 阿里雲oss的設定 */ private final AliyunOssProperties aliyunOssProperties; public AliyunFileOperator(AliyunOssProperties aliyunOssProperties) { this.aliyunOssProperties = aliyunOssProperties; this.initClient(); } @Override public void initClient() { String endpoint = aliyunOssProperties.getEndPoint(); String accessKeyId = aliyunOssProperties.getAccessKeyId(); String accessKeySecret = aliyunOssProperties.getAccessKeySecret(); // 建立OSSClient範例。 ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } @Override public void destroyClient() { ossClient.shutdown(); } @Override public Object getClient() { return ossClient; } @Override public boolean doesBucketExist(String bucketName) { try { return ossClient.doesBucketExist(bucketName); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public void setBucketAcl(String bucketName, BucketAuthEnum bucketAuthEnum) { try { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicReadWrite); } } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public boolean isExistingFile(String bucketName, String key) { try { return ossClient.doesObjectExist(bucketName, key); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public void storageFile(String bucketName, String key, byte[] bytes) { try { ossClient.putObject(bucketName, key, new ByteArrayInputStream(bytes)); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public void storageFile(String bucketName, String key, InputStream inputStream, long fileSize) { try { String contentType = "application/octet-stream"; if (key.contains(".")) { contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(key); } ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(contentType); metadata.setContentLength(fileSize); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata); ossClient.putObject(putObjectRequest); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public byte[] getFileBytes(String bucketName, String key) { InputStream objectContent = null; try { OSSObject ossObject = ossClient.getObject(bucketName, key); objectContent = ossObject.getObjectContent(); return IoUtil.readBytes(objectContent); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } finally { IoUtil.close(objectContent); } } @Override public void setFileAcl(String bucketName, String key, BucketAuthEnum bucketAuthEnum) { try { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicReadWrite); } } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public void copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) { try { ossClient.copyObject(originBucketName, originFileKey, newBucketName, newFileKey); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public String getFileAuthUrl(String bucketName, String key, Long timeoutMillis) { try { Date expiration = new Date(new Date().getTime() + timeoutMillis); URL url = ossClient.generatePresignedUrl(bucketName, key, expiration); return url.toString(); } catch (OSSException e) { throw new AliyunFileServiceException(e); } catch (ClientException e) { throw new AliyunFileServiceException(e); } } @Override public void deleteFile(String bucketName, String key) { ossClient.deleteObject(bucketName, key); } /** * 初始化分片檔案上傳 * * @param bucketName 檔案桶 * @param key 檔案key * @return 本次檔案上傳唯一標識 */ @Override public String initiateMultipartUpload(String bucketName, String key) { InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key); InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request); log.info("阿里雲 初始化分片檔案上傳:{}", key); return result.getUploadId(); } /** * 上傳分片檔案 * * @param fileUploadPart 分片檔案引數 * @return 上傳結果 */ @Override public FileUploadPartResult uploadPart(FileUploadPart fileUploadPart) { UploadPartRequest request = AliyunConvert.INSTANCE.convert(fileUploadPart); UploadPartResult result = ossClient.uploadPart(request); FileUploadPartResult convert = AliyunConvert.INSTANCE.convert(result); convert.setPartSize(fileUploadPart.getPartSize()); log.info("阿里雲 分片檔案上傳:{},結果:{}", fileUploadPart, request); return convert; } /** * 完成分片上傳 * * @param completeFileUploadPart 請求物件 * @return 結果資訊 */ @Override public CompleteFileUploadPartResult completeMultipartUpload(CompleteFileUploadPart completeFileUploadPart) { List<PartETag> tags = new ArrayList<>(); for (FileUploadPartResult partETag : completeFileUploadPart.getPartETags()) { tags.add(new PartETag(partETag.getPartNumber(), partETag.getPartETag())); } CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest( completeFileUploadPart.getBucketName(), completeFileUploadPart.getObjectName(), completeFileUploadPart.getUploadId(), tags ); CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(request); log.info("京東雲合併檔案:{},結果:{}", completeFileUploadPart, result); return AliyunConvert.INSTANCE.convert(result); } /** * 取消檔案分片上傳 * * @param bucketName 檔案桶 * @param objectName 物件key * @param uploadId 上傳ID * @return */ @Override public void abortMultipartUpload(String bucketName, String objectName, String uploadId) { AbortMultipartUploadRequest request = new AbortMultipartUploadRequest(bucketName, objectName, uploadId); ossClient.abortMultipartUpload(request); } }
package com.aimilin.common.base.file.modular.jdcloud; import cn.hutool.core.io.IoUtil; import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.model.*; import com.aimilin.common.base.file.FileOperator; import com.aimilin.common.base.file.FilePartOperator; import com.aimilin.common.base.file.common.enums.BucketAuthEnum; import com.aimilin.common.base.file.common.enums.FileLocationEnum; import com.aimilin.common.base.file.modular.jdcloud.exp.JdCloudFileServiceException; import com.aimilin.common.base.file.modular.jdcloud.prop.JdCloudConvert; import com.aimilin.common.base.file.modular.jdcloud.prop.JdCloudOssProperties; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.ClientConfiguration; import com.aimilin.common.base.file.param.*; import lombok.extern.slf4j.Slf4j; import javax.activation.MimetypesFileTypeMap; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 京東雲物件儲存 * * @version V1.0 */ @Slf4j public class JdCloudFileOperator implements FilePartOperator { @Override public FileLocationEnum getFileLocation() { return FileLocationEnum.JDCLOUD; } /** * 京東雲使用者端 */ private AmazonS3 ossClient; /** * 京東雲oss的設定 */ private final JdCloudOssProperties jdCloudOssProperties; /** * * @param jdCloudOssProperties */ public JdCloudFileOperator(JdCloudOssProperties jdCloudOssProperties) { this.jdCloudOssProperties = jdCloudOssProperties; this.initClient(); } @Override public void initClient() { ClientConfiguration config = new ClientConfiguration(); AwsClientBuilder.EndpointConfiguration endpointConfig = new AwsClientBuilder.EndpointConfiguration(jdCloudOssProperties.getEndPoint(), jdCloudOssProperties.getSigningRegion()); AWSCredentials awsCredentials = new BasicAWSCredentials(jdCloudOssProperties.getAccessKeyID(),jdCloudOssProperties.getAccessKeySecret()); AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials); ossClient = AmazonS3Client.builder() .withEndpointConfiguration(endpointConfig) .withClientConfiguration(config) .withCredentials(awsCredentialsProvider) .disableChunkedEncoding() .build(); } @Override public void destroyClient() { ossClient.shutdown(); } @Override public Object getClient() { return ossClient; } @Override public boolean doesBucketExist(String bucketName) { return ossClient.doesBucketExistV2(bucketName); } @Override public void setBucketAcl(String bucketName, BucketAuthEnum bucketAuthEnum) { try { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicReadWrite); } } catch (Exception e) { log.error("JdCloud-oss-設定預定義策略異常",e); throw new JdCloudFileServiceException(e); } } @Override public boolean isExistingFile(String bucketName, String key) { try { return ossClient.doesObjectExist(bucketName, key); } catch (Exception e) { log.error("JdCloud-oss-判斷是否存在檔案異常",e); throw new JdCloudFileServiceException(e); } } @Override public void storageFile(String bucketName, String key, byte[] bytes) { try { InputStream is = new ByteArrayInputStream(bytes); ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType("text/plain"); metadata.setContentLength((long)bytes.length); ossClient.putObject(bucketName, key, is, metadata); } catch (Exception e) { log.error("JdCloud-oss-儲存檔案異常",e); throw new JdCloudFileServiceException(e); } } @Override public void storageFile(String bucketName, String key, InputStream inputStream, long fileSize) { try { String contentType = "application/octet-stream"; if (key.contains(".")) { contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(key); } ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(contentType); metadata.setContentLength(fileSize); ossClient.putObject(bucketName, key, inputStream, metadata); } catch (Exception e) { log.error("JdCloud-oss-儲存檔案異常",e); throw new JdCloudFileServiceException(e); } } @Override public byte[] getFileBytes(String bucketName, String key) { InputStream objectContent = null; try { S3Object s3Object = ossClient.getObject(bucketName, key); objectContent = s3Object.getObjectContent(); return IoUtil.readBytes(objectContent); } catch (Exception e) { log.error("JdCloud-oss-獲取某個bucket下的檔案位元組異常",e); throw new JdCloudFileServiceException(e); }finally { IoUtil.close(objectContent); } } @Override public void setFileAcl(String bucketName, String key, BucketAuthEnum bucketAuthEnum) { try { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { ossClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicReadWrite); } } catch (Exception e) { log.error("JdCloud-oss-檔案存取許可權管理異常",e); throw new JdCloudFileServiceException(e); } } @Override public void copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) { try { ossClient.copyObject(originBucketName, originFileKey, newBucketName, newFileKey); } catch (Exception e) { log.error("JdCloud-oss-拷貝檔案異常",e); throw new JdCloudFileServiceException(e); } } @Override public String getFileAuthUrl(String bucketName, String key, Long timeoutMillis) { try { Date expiration = new Date(System.currentTimeMillis() + timeoutMillis); URL url = ossClient.generatePresignedUrl(bucketName, key, expiration); return url.toString(); } catch (Exception e) { log.error("JdCloud-oss-獲取檔案的下載地址異常",e); throw new JdCloudFileServiceException(e); } } @Override public void deleteFile(String bucketName, String key) { try { ossClient.deleteObject(bucketName, key); } catch (Exception e) { log.error("JdCloud-oss-刪除檔案異常", e); throw new JdCloudFileServiceException(e); } } /** * 初始化分片檔案上傳 * * @param bucketName 檔案桶 * @param key 檔案key * @return 本次檔案上傳唯一標識 */ @Override public String initiateMultipartUpload(String bucketName, String key) { InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key); InitiateMultipartUploadResult initiateMultipartUploadResult = ossClient.initiateMultipartUpload(request); log.info("京東雲 初始化分片檔案上傳:{}", key); return initiateMultipartUploadResult.getUploadId(); } /** * 上傳分片檔案 * * @param fileUploadPart 分片檔案引數 * @return 上傳結果 */ @Override public FileUploadPartResult uploadPart(FileUploadPart fileUploadPart) { UploadPartRequest request = JdCloudConvert.INSTANCE.convert(fileUploadPart); UploadPartResult uploadPartResult = ossClient.uploadPart(request); FileUploadPartResult result = JdCloudConvert.INSTANCE.convert(uploadPartResult.getPartETag()); result.setPartSize(fileUploadPart.getPartSize()); log.info("京東雲 分片檔案上傳:{},結果:{}", fileUploadPart, request); return result; } /** * 完成分片上傳 * * @param completeFileUploadPart 請求物件 * @return 結果資訊 */ @Override public CompleteFileUploadPartResult completeMultipartUpload(CompleteFileUploadPart completeFileUploadPart) { CompleteMultipartUploadRequest request = JdCloudConvert.INSTANCE.convert(completeFileUploadPart); CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(request); log.info("京東雲合併檔案:{},結果:{}", completeFileUploadPart, result); return JdCloudConvert.INSTANCE.convert(result); } /** * 取消檔案分片上傳 * * @param bucketName 檔案桶 * @param objectName 物件key * @param uploadId 上傳ID * @return */ @Override public void abortMultipartUpload(String bucketName, String objectName, String uploadId) { ossClient.abortMultipartUpload(new AbortMultipartUploadRequest(bucketName, objectName, uploadId)); } }
package com.aimilin.common.base.file.modular.tencent; import cn.hutool.core.io.IoUtil; import com.aimilin.common.base.file.FilePartOperator; import com.aimilin.common.base.file.common.enums.FileLocationEnum; import com.aimilin.common.base.file.modular.aliyun.prop.AliyunConvert; import com.aimilin.common.base.file.modular.tencent.prop.TenConvert; import com.aimilin.common.base.file.param.CompleteFileUploadPart; import com.aimilin.common.base.file.param.CompleteFileUploadPartResult; import com.aimilin.common.base.file.param.FileUploadPart; import com.aimilin.common.base.file.param.FileUploadPartResult; import com.qcloud.cos.COSClient; import com.qcloud.cos.ClientConfig; import com.qcloud.cos.auth.BasicCOSCredentials; import com.qcloud.cos.auth.COSCredentials; import com.qcloud.cos.exception.CosClientException; import com.qcloud.cos.exception.CosServiceException; import com.qcloud.cos.http.HttpMethodName; import com.qcloud.cos.model.*; import com.qcloud.cos.region.Region; import com.qcloud.cos.transfer.TransferManager; import com.qcloud.cos.transfer.TransferManagerConfiguration; import com.aimilin.common.base.file.FileOperator; import com.aimilin.common.base.file.common.enums.BucketAuthEnum; import com.aimilin.common.base.file.modular.tencent.exp.TencentFileServiceException; import com.aimilin.common.base.file.modular.tencent.prop.TenCosProperties; import lombok.extern.slf4j.Slf4j; import javax.activation.MimetypesFileTypeMap; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 騰訊雲內網檔案操作 * */ @Slf4j public class TenFileOperator implements FilePartOperator { @Override public FileLocationEnum getFileLocation() { return FileLocationEnum.TENCENT; } private final TenCosProperties tenCosProperties; private COSClient cosClient; private TransferManager transferManager; public TenFileOperator(TenCosProperties tenCosProperties) { this.tenCosProperties = tenCosProperties; initClient(); } @Override public void initClient() { // 1.初始化使用者身份資訊 String secretId = tenCosProperties.getSecretId(); String secretKey = tenCosProperties.getSecretKey(); COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); // 2.設定 bucket 的區域, COS 地域的簡稱請參照 https://cloud.tencent.com/document/product/436/6224 Region region = new Region(tenCosProperties.getRegionId()); ClientConfig clientConfig = new ClientConfig(region); // 3.生成 cos 使用者端。 cosClient = new COSClient(cred, clientConfig); // 4.執行緒池大小,建議在使用者端與 COS 網路充足(例如使用騰訊雲的 CVM,同地域上傳 COS)的情況下,設定成16或32即可,可較充分的利用網路資源 // 對於使用公網傳輸且網路頻寬質量不高的情況,建議減小該值,避免因網速過慢,造成請求超時。 ExecutorService threadPool = Executors.newFixedThreadPool(32); // 5.傳入一個 threadpool, 若不傳入執行緒池,預設 TransferManager 中會生成一個單執行緒的執行緒池。 transferManager = new TransferManager(cosClient, threadPool); // 6.設定高階介面的分塊上傳閾值和分塊大小為10MB TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration(); transferManagerConfiguration.setMultipartUploadThreshold(10 * 1024 * 1024); transferManagerConfiguration.setMinimumUploadPartSize(10 * 1024 * 1024); transferManager.setConfiguration(transferManagerConfiguration); } @Override public void destroyClient() { cosClient.shutdown(); } @Override public Object getClient() { return cosClient; } @Override public boolean doesBucketExist(String bucketName) { try { return cosClient.doesBucketExist(bucketName); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } } @Override public void setBucketAcl(String bucketName, BucketAuthEnum bucketAuthEnum) { try { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { cosClient.setBucketAcl(bucketName, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { cosClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { cosClient.setBucketAcl(bucketName, CannedAccessControlList.PublicReadWrite); } } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } } @Override public boolean isExistingFile(String bucketName, String key) { try { cosClient.getObjectMetadata(bucketName, key); return true; } catch (CosServiceException e) { return false; } } @Override public void storageFile(String bucketName, String key, byte[] bytes) { // 根據檔名獲取contentType String contentType = "application/octet-stream"; if (key.contains(".")) { contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(key); } // 上傳檔案 ByteArrayInputStream byteArrayInputStream = null; try { byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentType(contentType); cosClient.putObject(bucketName, key, new ByteArrayInputStream(bytes), objectMetadata); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } finally { IoUtil.close(byteArrayInputStream); } } @Override public void storageFile(String bucketName, String key, InputStream inputStream, long fileSize) { // 根據檔名獲取contentType String contentType = "application/octet-stream"; if (key.contains(".")) { contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(key); } // 上傳檔案 try { ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentType(contentType); objectMetadata.setContentLength(fileSize); cosClient.putObject(bucketName, key, inputStream, objectMetadata); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } finally { IoUtil.close(inputStream); } } @Override public byte[] getFileBytes(String bucketName, String key) { COSObjectInputStream cosObjectInput = null; try { GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key); COSObject cosObject = cosClient.getObject(getObjectRequest); cosObjectInput = cosObject.getObjectContent(); return IoUtil.readBytes(cosObjectInput); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } finally { IoUtil.close(cosObjectInput); } } @Override public void setFileAcl(String bucketName, String key, BucketAuthEnum bucketAuthEnum) { if (bucketAuthEnum.equals(BucketAuthEnum.PRIVATE)) { cosClient.setObjectAcl(bucketName, key, CannedAccessControlList.Private); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ)) { cosClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicRead); } else if (bucketAuthEnum.equals(BucketAuthEnum.PUBLIC_READ_WRITE)) { cosClient.setObjectAcl(bucketName, key, CannedAccessControlList.PublicReadWrite); } } @Override public void copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) { // 初始化拷貝引數 Region srcBucketRegion = new Region(tenCosProperties.getRegionId()); CopyObjectRequest copyObjectRequest = new CopyObjectRequest( srcBucketRegion, originBucketName, originFileKey, newBucketName, newFileKey); // 拷貝物件 try { transferManager.copy(copyObjectRequest, cosClient, null); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } } @Override public String getFileAuthUrl(String bucketName, String key, Long timeoutMillis) { GeneratePresignedUrlRequest presignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.GET); Date expirationDate = new Date(System.currentTimeMillis() + timeoutMillis); presignedUrlRequest.setExpiration(expirationDate); URL url = null; try { url = cosClient.generatePresignedUrl(presignedUrlRequest); } catch (CosServiceException e) { throw new TencentFileServiceException(e); } catch (CosClientException e) { throw new TencentFileServiceException(e); } return url.toString(); } @Override public void deleteFile(String bucketName, String key) { cosClient.deleteObject(bucketName, key); } /** * 初始化分片檔案上傳 * * @param bucketName 檔案桶 * @param key 檔案key * @return 本次檔案上傳唯一標識 */ @Override public String initiateMultipartUpload(String bucketName, String key) { InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key); InitiateMultipartUploadResult result = cosClient.initiateMultipartUpload(request); log.info("騰訊雲 初始化分片檔案上傳:{}", key); return result.getUploadId(); } /** * 上傳分片檔案 * * @param fileUploadPart 分片檔案引數 * @return 上傳結果 */ @Override public FileUploadPartResult uploadPart(FileUploadPart fileUploadPart) { UploadPartRequest request = TenConvert.INSTANCE.convert(fileUploadPart); UploadPartResult result = cosClient.uploadPart(request); FileUploadPartResult convert = TenConvert.INSTANCE.convert(result); convert.setPartSize(fileUploadPart.getPartSize()); log.info("騰訊雲 分片檔案上傳:{},結果:{}", fileUploadPart, request); return convert; } /** * 完成分片上傳 * * @param completeFileUploadPart 請求物件 * @return 結果資訊 */ @Override public CompleteFileUploadPartResult completeMultipartUpload(CompleteFileUploadPart completeFileUploadPart) { List<PartETag> tags = new ArrayList<>(); for (FileUploadPartResult partETag : completeFileUploadPart.getPartETags()) { tags.add(new PartETag(partETag.getPartNumber(), partETag.getPartETag())); } CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest( completeFileUploadPart.getBucketName(), completeFileUploadPart.getObjectName(), completeFileUploadPart.getUploadId(), tags); CompleteMultipartUploadResult result = cosClient.completeMultipartUpload(request); log.info("京東雲合併檔案:{},結果:{}", completeFileUploadPart, result); return TenConvert.INSTANCE.convert(result); } /** * 取消檔案分片上傳 * * @param bucketName 檔案桶 * @param objectName 物件key * @param uploadId 上傳ID * @return */ @Override public void abortMultipartUpload(String bucketName, String objectName, String uploadId) { AbortMultipartUploadRequest request = new AbortMultipartUploadRequest(bucketName, objectName, uploadId); cosClient.abortMultipartUpload(request); } }
<template> <div class="upload__wrap" :class="`upload__wrap--${size}`"> <div class="files" v-for="img in existsImgs" :key="img.id"> <template v-if="pictureType.includes(handleType(img.fileSuffix))"> <!-- 圖片型別 --> <img style="object-fit: cover;" width="104" height="104" :src="handleImg(img.id, 208, 208)" /> <div class="btn__wraps"> <div class="btn__innerwraps"> <a-icon class="icon__btn" type="eye" @click="$refs.previewForm.preview({ id: img.id })" /> <a-popconfirm placement="topRight" title="確認刪除?" @confirm="() => deleteImg(img.id)"> <a-icon class="icon__btn" type="delete" /> </a-popconfirm> </div> </div> </template> <template v-else-if="threedType.includes(handleType(img.fileSuffix))"> <img style="object-fit: cover;cursor: pointer;" width="104" height="104" src="https://s3.ap-northeast-1.wasabisys.com/img.it145.com/202206/testryo0zf0uh2l.png" /> <div class="btn__wraps"> <div class="btn__innerwraps"> <a-icon class="icon__btn" type="eye" @click="show3dModal(img)" /> <a-popconfirm placement="topRight" title="確認刪除?" @confirm="() => deleteImg(img.id)"> <a-icon class="icon__btn" type="delete" /> </a-popconfirm> </div> </div> </template> <template v-else> 當前型別檔案暫不支援預覽 </template> </div> <div class="tempimg__placeholder" v-for="temp in tempImgArr" :key="temp.uid">上傳中…</div> <a-upload name="upload" :list-type="listType" :file-list="fileList" :accept="format" :multiple="multiple" :before-upload="beforeUpload" :customRequest="customRequest" > <div v-if="existsImgs.length + tempImgArr.length < maxPicsLength"> <a-icon type="plus" /> <div class="ant-upload-text"> 上傳 </div> </div> </a-upload> <preview-form ref="previewForm"></preview-form> <preview3d-model :is3dModelShow="is3dModelShow" :carousel-lists="preview3dModel" title="3D模型預覽" @closeModal="closeModal"></preview3d-model> <!-- :carousel-lists="" --> </div> </template> <script> // import { sysFileInfoPage, sysFileInfoDelete, sysFileInfoPartUpload, sysFileInfoDownload } from '@/api/modular/system/fileManage' import { sysFileInfoPartUpload } from '@/api/modular/system/fileManage' import previewForm from '@/views/system/file/previewForm.vue' import Preview3dModel from '@/views/system/file/preview3dmodel.vue' import { handleImg } from '@/utils/util' import SparkMD5 from 'spark-md5' import { SUCCESS, SERVICE_ERROR, UPLOADING } from '@/assets/js/responseCode' const SIZEUNIT = 1 * 1024 * 1024 export default { components: { previewForm, Preview3dModel }, props: { isCloseUpload: { type: Boolean, default: false }, size: { type: String, default: 'default' }, format: { type: String, default: 'image/gif, image/jpeg, image/png, image/jpg' }, listType: { type: String, default: 'picture-card' }, maxPicsLength: { type: Number, default: 9 }, uploadText: { type: String, default: '上傳' }, existsImgs: { type: Array, default () { return [] } }, maxSize: { type: Number, default: 20 }, multiple: { type: Boolean, default: false } }, data() { return { pictureType: ['.gif', '.jpeg', '.png', '.jpg'], threedType: ['.json', '.obj', '.dae', '.ply', '.gltf', '.stl', '.fbx'], previewVisible: false, previewImage: '', fileList: [], // loading: false, is3dModelShow: false, preview3dModel: [], tempImgArr: [], isStopUpload: false } }, create() { this.timer = null console.log('this', this) }, watch: { isCloseUpload: { handler (newval) { if (newval) { this.$set(this, 'tempImgArr', []) this.$emit('imgUploadingStatus', 0) } }, immediate: true } }, methods: { handleImg, show3dModal (obj) { this.preview3dModel = [obj] this.is3dModelShow = true }, closeModal () { this.is3dModelShow = false }, handleType (filetType) { return filetType.indexOf('.') > -1 ? filetType : '.' + filetType }, beforeUpload(file, fileList) { console.log('this', this) return new Promise((resolve, reject) => { let type = file.type if (!type) { type = '.' + file.name.split('.').pop() } const isFormatFiles = this.format.replace(/s*/g, '').split(',').includes(type) if (!isFormatFiles) { this.$message.error(`只支援以下${this.format}格式!`) return reject(new Error(`只支援以下${this.format}格式!`)) } const maxSizeLimit = this.threedType.includes(type) ? 100 : 20 const isLtMaxSize = file.size / SIZEUNIT < maxSizeLimit if (!isLtMaxSize) { this.$message.error(`圖片須小於${maxSizeLimit}MB!`) return reject(new Error(`圖片須小於${maxSizeLimit}MB!`)) } // 是否上傳圖片超過最大限度 if (this.existsImgs.length + this.tempImgArr.length >= this.maxPicsLength) { if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { this.$message.error(`最多隻能上傳${this.maxPicsLength}張!`) }, 300) return reject(new Error(`最多隻能上傳${this.maxPicsLength}張!`)) } this.isStopUpload = false // this.loading = true this.$set(this, 'tempImgArr', [...this.tempImgArr, file.uid]) this.$emit('imgUploadingStatus', [...this.tempImgArr, file.uid].length) this.$emit('resetUploadStatus') resolve(true) }) // return isFormatFiles && isLt2M }, preview (id) { this.$refs.previewForm.preview({ id }) }, deleteImg (id) { this.$emit('deletePic', id) }, /** * 上傳檔案 */ customRequest (data) { const fileType = '.' + data.file.name.split('.').pop() const fileReader = new FileReader() const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice let currentChunk = 0 const chunkSize = 4 * 1024 * 1024 const chunks = Math.ceil(data.file.size / chunkSize) const spark = new SparkMD5.ArrayBuffer() const partChunksArr = [] const fileData = {} loadNext() fileReader.onload = e => { spark.append(e.target.result) const sparkChunk = new SparkMD5.ArrayBuffer() sparkChunk.append(e.target.result) const partMd5 = sparkChunk.end() partChunksArr.push({ file: fileData[currentChunk], partNumber: currentChunk + 1, partMd5, partSize: chunkSize, totalSize: data.file.size }) currentChunk++ if (currentChunk < chunks) { loadNext() } else { const md5 = spark.end() this.finalUploadFn(partChunksArr, fileType, data, md5) } } fileReader.onerror = function () { this.$message.error(`檔案${data.file.name}讀取出錯,請檢查該檔案`) // data.cancel() } function loadNext() { const start = currentChunk * chunkSize const end = ((start + chunkSize) >= data.file.size) ? data.file.size : start + chunkSize const currentChunkData = blobSlice.call(data.file, start, end) fileReader.readAsArrayBuffer(currentChunkData) fileData[currentChunk] = currentChunkData } }, finalUploadFn (formData, fileType, data, wholeFileMd5) { formData.forEach(item => { const newFormData = new FormData() // newFormData.set('file', data.file) newFormData.set('uid', data.file.uid) newFormData.set('filename', data.file.name) Object.keys(item).forEach(key => { newFormData.set( key, item[key] ) newFormData.set('fileMd5', wholeFileMd5) }) if (this.isStopUpload) { return } sysFileInfoPartUpload(newFormData).then((res) => { // this.loading = false if (res.code === SUCCESS && res.data?.fileState === SUCCESS) { this.$emit('getNewPics', { id: res.data.fileId, fileSuffix: fileType }) const newTempImgArr = this.tempImgArr.filter(item => item !== res.data?.uid) this.$set(this, 'tempImgArr', newTempImgArr) this.$emit('imgUploadingStatus', newTempImgArr.length) // this.$refs.table.refresh() } else if (res.code === SUCCESS && res.data?.fileState === UPLOADING) { } else if (res.code === SUCCESS && res.data?.fileState === SERVICE_ERROR) { if (!this.failupload) { this.failupload = {} this.failupload[data.file.uid] = data.file.uid sysFileInfoPartUpload(newFormData) } else { if (!this.failupload[data.file.uid]) { sysFileInfoPartUpload(newFormData) this.failupload[data.file.uid] = data.file.uid } } } else if (res.code !== SUCCESS) { // 上傳失敗,從佔點陣圖中移除一個 const newTempImgArr = this.tempImgArr newTempImgArr.pop() this.$set(this, 'tempImgArr', newTempImgArr) this.$emit('imgUploadingStatus', newTempImgArr.length) if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { this.$message.error('上傳失敗!' + res.message) }, 300) } }).catch(e => { const newTempImgArr = this.tempImgArr newTempImgArr.pop() this.$set(this, 'tempImgArr', newTempImgArr) this.$emit('imgUploadingStatus', newTempImgArr.length) console.log('error', e) // this.loading = false // this.tempImgArr.length && this.$message.error('上傳失敗,請重新上傳') }).finally((p) => { console.log('sysFileInfoPartUpload', p) // this.loading = false }) }) }, clearTimer() { clearTimeout(this.timer) this.$set(this, 'tempImgArr', []) this.$emit('imgUploadingStatus', 0) this.isStopUpload = true } }, beforeDestoryed() { this.clearTimer() } } </script> <style> /* you can make up upload button and sample style by using stylesheets */ .ant-upload-select-picture-card i { font-size: 32px; color: #999; } .ant-upload-select-picture-card .ant-upload-text { margin-top: 8px; color: #666; } </style> <style lang="less" scoped> .upload__wrap{ display: -webkit-inline-box; display: -moz-inline-box; display: inline-box; flex-wrap: wrap; .files{ position: relative; width:104px; height: 104px; margin-right: 10px; margin-bottom: 10px; .btn__wraps{ position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0); display: flex; align-items: center; justify-content: center; transition: all 0.3s linear; z-index: -1; .btn__innerwraps{ display: flex; .icon__btn{ margin-right: 10px; font-size: 16px; color: rgba(255,255,255, 0); cursor: pointer; &:last-child{ margin-right: 0; } } } } &:hover{ .btn__wraps{ background: rgba(0,0,0,0.5); transition: all 0.3s linear; z-index: 1; .btn__innerwraps{ .icon__btn{ color: rgba(255,255,255, 0.8); } } } } } .tempimg__placeholder{ width: 104px; height: 104px; display: flex; justify-content: center; align-items: center; border: 1px solid #d9d9d9; margin-right: 10px; margin-bottom: 10px; } } </style>
參考資料:
專案參考地址:https://gitee.com/donghuangtaiyi/file-uploader
到此這篇關於Java超詳細大檔案分片上傳程式碼的文章就介紹到這了,更多相關Java大檔案上傳內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45