package com.webank.weevent.governance.service; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletRequest; import com.webank.weevent.client.BrokerException; import com.webank.weevent.core.config.FiscoConfig; import com.webank.weevent.file.IWeEventFileClient; import com.webank.weevent.file.dto.FileChunksMetaStatus; import com.webank.weevent.file.dto.FileTransportStats; import com.webank.weevent.file.inner.DiskFiles; import com.webank.weevent.file.service.FileChunksMeta; import com.webank.weevent.governance.GovernanceApplication; import com.webank.weevent.governance.common.ConstantProperties; import com.webank.weevent.governance.common.ErrorCode; import com.webank.weevent.governance.common.GovernanceException; import com.webank.weevent.governance.common.GovernanceResult; import com.webank.weevent.governance.entity.FileTransportEntity; import com.webank.weevent.governance.entity.UploadChunkParam; import com.webank.weevent.governance.repository.TransportRepository; import com.webank.weevent.governance.utils.ParamCheckUtils; import com.webank.weevent.governance.utils.Utils; import javafx.util.Pair; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.commons.CommonsMultipartResolver; /** * file upload/download Service. * * @author v_wbhwliu * @version 1.3 * @since 2020/5/20 */ @Service @Slf4j public class FileService { private TransportRepository transportRepository; // file upload root path private String uploadPath; // file download root path private String downloadPath; // <brokerId, <groupId, <IWeEventFileClient, DiskFiles>>> private final Map<Integer, Map<String, Pair<IWeEventFileClient, DiskFiles>>> fileClientMap = new ConcurrentHashMap<>(); // <brokerId, <groupId, <topic, overwrite>>> private final Map<Integer, Map<String, Map<String, Boolean>>> transportMap = new ConcurrentHashMap<>(); // upload local file to governance server, <fileId, FileChunksMeta> private final Map<String, Pair<FileChunksMeta, DiskFiles>> fileChunksMap = new ConcurrentHashMap<>(); @Autowired public void setTransportRepository(TransportRepository transportRepository) throws GovernanceException { this.transportRepository = transportRepository; this.uploadPath = GovernanceApplication.governanceConfig.getFileTransportPath() + File.separator + ConstantProperties.UPLOAD; this.downloadPath = GovernanceApplication.governanceConfig.getFileTransportPath() + File.separator + ConstantProperties.DOWNLOAD; this.initFileTransportBasePath(); this.syncTransportToCache(this.transportRepository.findAll()); } public GovernanceResult openTransport(FileTransportEntity fileTransport) throws GovernanceException { ParamCheckUtils.validateTransportName(fileTransport.getTopicName()); ParamCheckUtils.validateTransportRole(fileTransport.getRole()); ParamCheckUtils.validateOverWrite(fileTransport.getOverWrite()); this.checkTransportExist(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileTransport.getTopicName()); if (ConstantProperties.TRANSPORT_RECEIVER.equals(fileTransport.getRole())) { this.openTransport4Receiver(fileTransport); } else { this.openTransport4Sender(fileTransport); } this.transportRepository.save(fileTransport); return GovernanceResult.ok(true); } private void openTransport4Sender(FileTransportEntity fileTransport) throws GovernanceException { IWeEventFileClient fileClient; try { fileClient = this.buildIWeEventFileClient(fileTransport.getGroupId(), fileTransport.getBrokerId()); if (StringUtils.isBlank(fileTransport.getPublicKey())) { fileClient.openTransport4Sender(fileTransport.getTopicName()); } else { fileClient.openTransport4Sender(fileTransport.getTopicName(), new ByteArrayInputStream(fileTransport.getPublicKey().getBytes(StandardCharsets.UTF_8))); } } catch (BrokerException e) { log.error("open sender transport failed.", e); this.fileClientMap.remove(fileTransport.getBrokerId()); throw new GovernanceException(e.getMessage()); } this.addIWeEventClientToCache(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileClient); this.addTransportToCache(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileTransport.getTopicName(), fileTransport.getOverWrite()); log.info("open sender transport success, groupId:{}, topic:{}", fileTransport.getGroupId(), fileTransport.getTopicName()); } private void openTransport4Receiver(FileTransportEntity fileTransport) throws GovernanceException { IWeEventFileClient fileClient; try { fileClient = buildIWeEventFileClient(fileTransport.getGroupId(), fileTransport.getBrokerId()); if (StringUtils.isBlank(fileTransport.getPrivateKey())) { fileClient.openTransport4Receiver(fileTransport.getTopicName(), new IWeEventFileClient.FileListener() { @Override public void onFile(String topicName, String fileName) { log.info("receive file:{} from topic: {}.", fileName, topicName); } @Override public void onException(Throwable e) { log.error("onException:", e); } }); } else { fileClient.openTransport4Receiver(fileTransport.getTopicName(), new IWeEventFileClient.FileListener() { @Override public void onFile(String topicName, String fileName) { log.info("receive file:{} from topic: {}.", fileName, topicName); } @Override public void onException(Throwable e) { log.error("onException:", e); } }, new ByteArrayInputStream(fileTransport.getPrivateKey().getBytes(StandardCharsets.UTF_8))); } } catch (BrokerException e) { log.error("open receive transport failed.", e); this.fileClientMap.remove(fileTransport.getBrokerId()); throw new GovernanceException(e.getMessage()); } this.addIWeEventClientToCache(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileClient); this.addTransportToCache(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileTransport.getTopicName(), fileTransport.getOverWrite()); log.info("open receiver transport success, groupId:{}, topic:{}", fileTransport.getGroupId(), fileTransport.getTopicName()); } public GovernanceResult uploadFile(HttpServletRequest request) throws GovernanceException { UploadChunkParam chunkParam = parseUploadChunkRequest(request); log.info("upload chunk:{}", chunkParam); IWeEventFileClient fileClient = this.getIWeEventFileClient(chunkParam.getFileChunksMeta().getGroupId(), chunkParam.getBrokerId()); boolean isSuccess = this.uploadChunks(chunkParam.getFileChunksMeta(), chunkParam.getChunkNumber(), chunkParam.getChunkData()); // if chunk upload failed, sleep and retry again for (int i = 1; i <= ConstantProperties.UPLOAD_CHUNK_FAIL_RETRY_COUNT; i++) { if (isSuccess) { break; } isSuccess = this.uploadChunks(chunkParam.getFileChunksMeta(), chunkParam.getChunkNumber(), chunkParam.getChunkData()); } if (!isSuccess) { throw new GovernanceException(ErrorCode.FILE_UPLOAD_FAILED); } if (chunkParam.getFileChunksMeta().checkChunkFull()) { CompletableFuture.runAsync(() -> { String fileId = chunkParam.getFileChunksMeta().getFileId(); String filePath = this.uploadPath.concat(File.separator).concat(fileId).concat(File.separator).concat(chunkParam .getFileChunksMeta().getTopic()).concat(File.separator).concat(chunkParam.getFileChunksMeta().getFileName()); boolean overWrite = this.transportMap.get(chunkParam.getBrokerId()).get(chunkParam.getFileChunksMeta().getGroupId()) .get(chunkParam.getFileChunksMeta().getTopic()); log.info("all chunks has uploaded success, start publish file, filePath:{}", filePath); try { fileClient.publishFile(chunkParam.getFileChunksMeta().getTopic(), filePath, overWrite); log.info("publish file success, topic:{}, fileName:{}.", chunkParam.getFileChunksMeta().getTopic(), chunkParam.getFileChunksMeta().getFileName()); } catch (BrokerException | IOException e) { log.error("publish file error, fileName:{}.", chunkParam.getFileChunksMeta().getFileName(), e); } finally { // remove local file after publish Utils.removeLocalFile(this.uploadPath.concat(File.separator).concat(fileId)); this.fileChunksMap.remove(fileId); log.info("remove local file after publish, fileName:topic:{}, fileName:{}.", chunkParam.getFileChunksMeta().getTopic(), chunkParam.getFileChunksMeta().getFileName()); } }); } return GovernanceResult.ok(true); } private UploadChunkParam parseUploadChunkRequest(HttpServletRequest request) throws GovernanceException { UploadChunkParam chunkParam = new UploadChunkParam(); String fileId = request.getParameter("identifier"); FileChunksMeta fileChunksMeta = this.fileChunksMap.get(fileId).getKey(); chunkParam.setFileChunksMeta(fileChunksMeta); chunkParam.setBrokerId(Integer.parseInt(request.getParameter("brokerId"))); chunkParam.setChunkNumber(Integer.parseInt(request.getParameter("chunkNumber")) - 1); chunkParam.setFileId(fileId); CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(request.getSession().getServletContext()); if (multipartResolver.isMultipart(request)) { MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request; Iterator<String> iter = multiRequest.getFileNames(); while (iter.hasNext()) { MultipartFile file = multiRequest.getFile(iter.next()); if (!Objects.isNull(file)) { try { chunkParam.setChunkData(file.getBytes()); } catch (IOException e) { log.error("parse upload chunk data error.", e); throw new GovernanceException(ErrorCode.PARSE_CHUNK_REQUEST_ERROR); } } } } return chunkParam; } public String downloadFile(String groupId, Integer brokerId, String fileId) { DiskFiles diskFiles = this.getDiskFiles(groupId, brokerId); return diskFiles.genLocalFileName(fileId); } public GovernanceResult listFile(String groupId, Integer brokerId, String topic) throws GovernanceException { IWeEventFileClient fileClient = getIWeEventFileClient(groupId, brokerId); try { List<FileChunksMeta> fileChunksMetas = fileClient.listFiles(topic); return GovernanceResult.ok(fileChunksMetas); } catch (BrokerException e) { log.error("list file error, topic:{}", topic); throw new GovernanceException(e.getMessage()); } } public GovernanceResult status(String groupId, Integer brokerId, String topic, String role) throws GovernanceException { ParamCheckUtils.validateTransportRole(role); List<FileChunksMetaStatus> fileChunksMetaStatusList = null; IWeEventFileClient fileClient = this.getIWeEventFileClient(groupId, brokerId); FileTransportStats status = fileClient.status(topic); if (ConstantProperties.TRANSPORT_RECEIVER.equals(role)) { if (status.getReceiver().containsKey(groupId)) { fileChunksMetaStatusList = status.getReceiver().get(groupId).get(topic); } } else { if (status.getSender().containsKey(groupId)) { fileChunksMetaStatusList = status.getSender().get(groupId).get(topic); } } return GovernanceResult.ok(fileChunksMetaStatusList); } public GovernanceResult listTransport(String groupId, Integer brokerId) { List<FileTransportEntity> fileTransportList = this.transportRepository.queryByBrokerIdAndGroupId(brokerId, groupId); fileTransportList.forEach(fileTransport -> { fileTransport.setCreateTime(Utils.dateToStr(fileTransport.getCreateDate())); if (StringUtils.isNotBlank(fileTransport.getPublicKey()) || StringUtils.isNotBlank(fileTransport.getPrivateKey())) { fileTransport.setVerified(true); } }); log.info("get transport list success, transport.size:{}", fileTransportList.size()); return GovernanceResult.ok(fileTransportList); } public GovernanceResult closeTransport(FileTransportEntity fileTransport) throws GovernanceException { IWeEventFileClient fileClient = this.getIWeEventFileClient(fileTransport.getGroupId(), fileTransport.getBrokerId()); fileClient.closeTransport(fileTransport.getTopicName()); this.transportRepository.delete(fileTransport); this.removeTransportCache(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileTransport.getTopicName()); return GovernanceResult.ok(true); } public GovernanceResult prepareUploadFile(String fileId, String filename, String topic, String groupId, long totalSize, Integer chunkSize) throws GovernanceException { if (this.fileChunksMap.containsKey(fileId)) { FileChunksMeta fileChunksMeta = this.fileChunksMap.get(fileId).getKey(); return GovernanceResult.ok(this.chunkUploadedList(fileChunksMeta)); } FileChunksMeta fileChunksMeta = new FileChunksMeta(fileId, filename, totalSize, "", topic, groupId, true); fileChunksMeta.initChunkSize(chunkSize); DiskFiles diskFiles = new DiskFiles(this.uploadPath + File.separator + fileId); try { diskFiles.createFixedLengthFile(fileChunksMeta); diskFiles.saveFileMeta(fileChunksMeta); } catch (BrokerException e) { log.error("create fileChunksMeta error.fileName:{}, fileId:{}.", filename, fileId, e); throw new GovernanceException(e.getMessage()); } this.fileChunksMap.put(fileId, new Pair<>(fileChunksMeta, diskFiles)); return GovernanceResult.ok(chunkUploadedList(fileChunksMeta)); } public void genPemFile(String groupId, Integer brokerId, String pemPath) throws GovernanceException { IWeEventFileClient fileClient = getIWeEventFileClient(groupId, brokerId); try { fileClient.genPemFile(pemPath); } catch (BrokerException e) { log.error("genPemFile error, pemPath:{}.", pemPath, e); throw new GovernanceException(ErrorCode.GENERATE_PEM_FAILED); } } private IWeEventFileClient buildIWeEventFileClient(String groupId, Integer brokerId) { if (!this.fileClientMap.containsKey(brokerId) || !this.fileClientMap.get(brokerId).containsKey(groupId)) { FiscoConfig fiscoConfig = new FiscoConfig(); fiscoConfig.load(""); IWeEventFileClient fileClient = IWeEventFileClient.build(groupId, this.downloadPath, ConstantProperties.FILE_CHUNK_SIZE, fiscoConfig); Map<String, Pair<IWeEventFileClient, DiskFiles>> fileClientOfEachGroupMap = new ConcurrentHashMap<>(); fileClientOfEachGroupMap.put(groupId, new Pair<>(fileClient, fileClient.getDiskFiles())); this.fileClientMap.put(brokerId, fileClientOfEachGroupMap); } return this.fileClientMap.get(brokerId).get(groupId).getKey(); } private IWeEventFileClient getIWeEventFileClient(String groupId, Integer brokerId) throws GovernanceException { if (!this.fileClientMap.containsKey(brokerId) || !this.fileClientMap.get(brokerId).containsKey(groupId)) { throw new GovernanceException(ErrorCode.TRANSPORT_NOT_EXISTS); } return this.fileClientMap.get(brokerId).get(groupId).getKey(); } private DiskFiles getDiskFiles(String groupId, Integer brokerId) { return this.fileClientMap.get(brokerId).get(groupId).getValue(); } private boolean uploadChunks(FileChunksMeta fileChunksMeta, Integer chunkIdx, byte[] chunkData) { DiskFiles diskFiles = this.fileChunksMap.get(fileChunksMeta.getFileId()).getValue(); try { diskFiles.writeChunkData(fileChunksMeta.getFileId(), chunkIdx, chunkData); this.fileChunksMap.get(fileChunksMeta.getFileId()).getKey().getChunkStatus().set(chunkIdx); log.info("upload file chunk data success, {}@{}.", fileChunksMeta.getFileId(), chunkIdx); } catch (BrokerException e) { log.error("write chunk data error, topic:{}, {}@{}", fileChunksMeta.getTopic(), fileChunksMeta.getFileId(), chunkIdx, e); return false; } return true; } private void checkTransportExist(Integer brokerId, String groupId, String topic) throws GovernanceException { if (this.transportMap.containsKey(brokerId) && this.transportMap.get(brokerId).containsKey(groupId) && this.transportMap.get(brokerId).get(groupId).containsKey(topic)) { throw new GovernanceException(ErrorCode.TRANSPORT_ALREADY_EXISTS); } } private void syncTransportToCache(List<FileTransportEntity> transportList) throws GovernanceException { for (FileTransportEntity fileTransport : transportList) { try { checkTransportExist(fileTransport.getBrokerId(), fileTransport.getGroupId(), fileTransport.getTopicName()); if (ConstantProperties.TRANSPORT_RECEIVER.equals(fileTransport.getRole())) { this.openTransport4Receiver(fileTransport); } else { this.openTransport4Sender(fileTransport); } } catch (GovernanceException e) { if (e.getCode() != ErrorCode.TRANSPORT_ALREADY_EXISTS.getCode()) { throw e; } } } log.info("synchronize transport from db to local cache success."); } private void initFileTransportBasePath() { File uploadFile = new File(this.uploadPath); if (!uploadFile.exists()) { uploadFile.mkdir(); } File downloadFile = new File(this.downloadPath); if (!downloadFile.exists()) { downloadFile.mkdir(); } } private void addIWeEventClientToCache(Integer brokerId, String groupId, IWeEventFileClient fileClient) { Map<String, Pair<IWeEventFileClient, DiskFiles>> clientMap = this.fileClientMap.get(brokerId); if (Objects.isNull(clientMap)) { clientMap = new ConcurrentHashMap<>(); } clientMap.put(groupId, new Pair<>(fileClient, fileClient.getDiskFiles())); this.fileClientMap.put(brokerId, clientMap); } private void addTransportToCache(Integer brokerId, String groupId, String topic, String overwrite) { // 0 means false, 1 means true; boolean isOverWrite = "1".equals(overwrite); if (this.transportMap.isEmpty()) { Map<String, Map<String, Boolean>> group2TransportMap = new ConcurrentHashMap<>(); Map<String, Boolean> transport2OverWriteMap = new HashMap<>(); transport2OverWriteMap.put(topic, isOverWrite); group2TransportMap.put(groupId, transport2OverWriteMap); this.transportMap.put(brokerId, group2TransportMap); return; } if (!this.transportMap.containsKey(brokerId)) { Map<String, Map<String, Boolean>> group2TransportMap = new ConcurrentHashMap<>(); Map<String, Boolean> transport2OverWriteMap = new HashMap<>(); transport2OverWriteMap.put(topic, isOverWrite); group2TransportMap.put(groupId, transport2OverWriteMap); this.transportMap.put(brokerId, group2TransportMap); return; } if (!this.transportMap.get(brokerId).containsKey(groupId)) { Map<String, Boolean> transport2OverWriteMap = new HashMap<>(); transport2OverWriteMap.put(topic, isOverWrite); this.transportMap.get(brokerId).put(groupId, transport2OverWriteMap); } else { this.transportMap.get(brokerId).get(groupId).put(topic, isOverWrite); } } private void removeTransportCache(Integer brokerId, String groupId, String topic) { if (Objects.isNull(this.transportMap.get(brokerId)) || Objects.isNull(this.transportMap.get(brokerId).get(groupId))) { log.error("transport:{} not exists in local cache.", topic); } else { this.transportMap.get(brokerId).get(groupId).remove(topic); } } private List<Integer> chunkUploadedList(FileChunksMeta fileChunksMeta) { List<Integer> uploadedChunks = new ArrayList<>(); for (int i = 0; i < fileChunksMeta.getChunkNum(); i++) { if (fileChunksMeta.getChunkStatus().get(i)) { uploadedChunks.add(i + 1); } } return uploadedChunks; } }