修改下载文件逻辑.

This commit is contained in:
lijiahang
2024-06-04 20:01:05 +08:00
parent 59d9739f36
commit 7f24948efa
16 changed files with 234 additions and 112 deletions

View File

@@ -97,4 +97,6 @@ public interface ErrorMessage {
String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置"; String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置";
String CLIENT_ABORT = "手动中断";
} }

View File

@@ -63,13 +63,14 @@ public class HostSftpLogController {
@PermitAll @PermitAll
@IgnoreWrapper @IgnoreWrapper
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/download") @GetMapping("/download")
@Operation(summary = "下载文件") @Operation(summary = "下载文件")
@Parameter(name = "channelId", description = "channelId", required = true) @Parameter(name = "channelId", description = "channelId", required = true)
@Parameter(name = "transferToken", description = "transferToken", required = true) @Parameter(name = "transferToken", description = "transferToken", required = true)
public StreamingResponseBody downloadFile(@RequestParam("channelId") String channelId, public StreamingResponseBody downloadWithTransferToken(@RequestParam("channelId") String channelId,
@RequestParam("transferToken") String transferToken, @RequestParam("transferToken") String transferToken,
HttpServletResponse response) { HttpServletResponse response) {
return hostSftpService.downloadWithTransferToken(channelId, transferToken, response); return hostSftpService.downloadWithTransferToken(channelId, transferToken, response);
} }

View File

@@ -30,9 +30,9 @@ public enum TransferOperatorType {
UPLOAD_ERROR(TransferOperatorType.UPLOAD, "uploadError"), UPLOAD_ERROR(TransferOperatorType.UPLOAD, "uploadError"),
/** /**
* 开始下载 * 初始化下载
*/ */
DOWNLOAD_START(TransferOperatorType.DOWNLOAD, "downloadStart"), DOWNLOAD_INIT(TransferOperatorType.DOWNLOAD, "downloadInit"),
/** /**
* 中断下载 * 中断下载

View File

@@ -34,11 +34,6 @@ public enum TransferReceiverType {
*/ */
UPLOAD_ERROR("uploadError"), UPLOAD_ERROR("uploadError"),
/**
* 下载完成
*/
DOWNLOAD_FINISH("downloadFinish"),
/** /**
* 开始下载 * 开始下载
*/ */
@@ -49,6 +44,11 @@ public enum TransferReceiverType {
*/ */
DOWNLOAD_PROGRESS("downloadProgress"), DOWNLOAD_PROGRESS("downloadProgress"),
/**
* 下载完成
*/
DOWNLOAD_FINISH("downloadFinish"),
/** /**
* 下载失败 * 下载失败
*/ */

View File

@@ -1,5 +1,6 @@
package com.orion.visor.module.asset.handler.host.transfer.handler; package com.orion.visor.module.asset.handler.host.transfer.handler;
import com.orion.lang.id.UUIds;
import com.orion.lang.utils.Exceptions; import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.io.Streams; import com.orion.lang.utils.io.Streams;
import com.orion.net.host.SessionStore; import com.orion.net.host.SessionStore;
@@ -14,6 +15,7 @@ import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperator
import com.orion.visor.module.asset.handler.host.transfer.session.*; import com.orion.visor.module.asset.handler.host.transfer.session.*;
import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils; import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils;
import com.orion.visor.module.asset.service.HostTerminalService; import com.orion.visor.module.asset.service.HostTerminalService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
@@ -45,10 +47,14 @@ public class TransferHandler implements ITransferHandler {
*/ */
private final ConcurrentHashMap<String, ITransferHostSession> sessions; private final ConcurrentHashMap<String, ITransferHostSession> sessions;
@Getter
private final ConcurrentHashMap<String, IDownloadSession> tokenSessions;
public TransferHandler(WebSocketSession channel) { public TransferHandler(WebSocketSession channel) {
this.channel = channel; this.channel = channel;
this.userId = WebSockets.getAttr(channel, ExtraFieldConst.USER_ID); this.userId = WebSockets.getAttr(channel, ExtraFieldConst.USER_ID);
this.sessions = new ConcurrentHashMap<>(); this.sessions = new ConcurrentHashMap<>();
this.tokenSessions = new ConcurrentHashMap<>();
} }
@Override @Override
@@ -73,9 +79,11 @@ public class TransferHandler implements ITransferHandler {
// 上传失败 // 上传失败
((IUploadSession) currentSession).uploadError(); ((IUploadSession) currentSession).uploadError();
break; break;
case DOWNLOAD_START: case DOWNLOAD_INIT:
// 开始下载 // 开始下载
((IDownloadSession) currentSession).startDownload(payload.getPath()); String token = UUIds.random32();
tokenSessions.put(token, (IDownloadSession) currentSession);
((IDownloadSession) currentSession).downloadInit(payload.getPath(), token);
break; break;
case DOWNLOAD_ABORT: case DOWNLOAD_ABORT:
// 中断下载 // 中断下载
@@ -100,7 +108,7 @@ public class TransferHandler implements ITransferHandler {
*/ */
private boolean getAndInitSession(TransferOperatorRequest payload, TransferOperatorType type) { private boolean getAndInitSession(TransferOperatorRequest payload, TransferOperatorType type) {
Long hostId = payload.getHostId(); Long hostId = payload.getHostId();
String sessionKey = hostId + "_" + type.getOperator(); String sessionKey = hostId + "_" + type.getKind();
try { try {
// 获取会话 // 获取会话
ITransferHostSession session = sessions.get(sessionKey); ITransferHostSession session = sessions.get(sessionKey);
@@ -109,10 +117,10 @@ public class TransferHandler implements ITransferHandler {
HostTerminalConnectDTO connectInfo = hostTerminalService.getTerminalConnectInfo(this.userId, hostId); HostTerminalConnectDTO connectInfo = hostTerminalService.getTerminalConnectInfo(this.userId, hostId);
SessionStore sessionStore = hostTerminalService.openSessionStore(connectInfo); SessionStore sessionStore = hostTerminalService.openSessionStore(connectInfo);
// 打开会话并初始化 // 打开会话并初始化
if (TransferOperatorType.UPLOAD.equals(type.getOperator())) { if (TransferOperatorType.UPLOAD.equals(type.getKind())) {
// 上传操作 // 上传操作
session = new UploadSession(connectInfo, sessionStore, this.channel); session = new UploadSession(connectInfo, sessionStore, this.channel);
} else if (TransferOperatorType.DOWNLOAD.equals(type.getOperator())) { } else if (TransferOperatorType.DOWNLOAD.equals(type.getKind())) {
// 下载操作 // 下载操作
session = new DownloadSession(connectInfo, sessionStore, this.channel); session = new DownloadSession(connectInfo, sessionStore, this.channel);
} else { } else {
@@ -136,6 +144,7 @@ public class TransferHandler implements ITransferHandler {
public void close() { public void close() {
log.info("TransferHandler.close channelId: {}", channel.getId()); log.info("TransferHandler.close channelId: {}", channel.getId());
sessions.values().forEach(Streams::close); sessions.values().forEach(Streams::close);
tokenSessions.clear();
} }
} }

View File

@@ -20,6 +20,9 @@ import lombok.NoArgsConstructor;
@Schema(name = "FileOperatorResponse", description = "文件操作响应 实体对象") @Schema(name = "FileOperatorResponse", description = "文件操作响应 实体对象")
public class TransferOperatorResponse { public class TransferOperatorResponse {
@Schema(description = "channelId")
private String channelId;
@Schema(description = "type") @Schema(description = "type")
private String type; private String type;
@@ -29,6 +32,12 @@ public class TransferOperatorResponse {
@Schema(description = "是否成功") @Schema(description = "是否成功")
private Boolean success; private Boolean success;
@Schema(description = "传输的大小")
private Integer currentSize;
@Schema(description = "transferToken")
private String transferToken;
@Schema(description = "消息") @Schema(description = "消息")
private String msg; private String msg;

View File

@@ -1,5 +1,6 @@
package com.orion.visor.module.asset.handler.host.transfer.session; package com.orion.visor.module.asset.handler.host.transfer.session;
import com.orion.lang.define.wrapper.Ref;
import com.orion.lang.utils.Threads; import com.orion.lang.utils.Threads;
import com.orion.lang.utils.Valid; import com.orion.lang.utils.Valid;
import com.orion.lang.utils.io.Streams; import com.orion.lang.utils.io.Streams;
@@ -12,11 +13,13 @@ import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
import com.orion.visor.module.asset.entity.dto.HostTerminalConnectDTO; import com.orion.visor.module.asset.entity.dto.HostTerminalConnectDTO;
import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType; import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType;
import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils; import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
/** /**
* 下载会话实现 * 下载会话实现
@@ -28,6 +31,9 @@ import java.io.InputStream;
@Slf4j @Slf4j
public class DownloadSession extends TransferHostSession implements IDownloadSession { public class DownloadSession extends TransferHostSession implements IDownloadSession {
@Getter
private String path;
private InputStream inputStream; private InputStream inputStream;
public DownloadSession(HostTerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) { public DownloadSession(HostTerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) {
@@ -35,7 +41,8 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
} }
@Override @Override
public void startDownload(String path) { public void downloadInit(String path, String token) {
this.path = path;
String channelId = channel.getId(); String channelId = channel.getId();
try { try {
log.info("DownloadSession.startDownload open start channelId: {}, path: {}", channelId, path); log.info("DownloadSession.startDownload open start channelId: {}, path: {}", channelId, path);
@@ -54,39 +61,17 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
} }
// 打开输入流 // 打开输入流
this.inputStream = executor.openInputStream(path); this.inputStream = executor.openInputStream(path);
// 响应开始下载
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_START, null, e -> {
e.setChannelId(channelId);
e.setTransferToken(token);
});
log.info("DownloadSession.startDownload open success channelId: {}, path: {}", channelId, path); log.info("DownloadSession.startDownload open success channelId: {}, path: {}", channelId, path);
} catch (Exception e) { } catch (Exception e) {
log.error("DownloadSession.startDownload open error channelId: {}, path: {}", channelId, path, e); log.error("DownloadSession.startDownload open error channelId: {}, path: {}", channelId, path, e);
// 响应结果 // 响应下载失败
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, e); TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, e);
return;
} }
// 异步读取文件内容
AssetThreadPools.TERMINAL_OPERATOR.execute(() -> {
Exception ex = null;
try {
byte[] buffer = new byte[Const.BUFFER_KB_32];
int len;
// 响应文件内容
while (this.inputStream != null && (len = this.inputStream.read(buffer)) != -1) {
this.channel.sendMessage(new BinaryMessage(buffer, 0, len, true));
}
log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path);
} catch (Exception e) {
log.error("DownloadSession.download error channelId: {}, path: {}", channelId, path, e);
ex = e;
}
// 关闭等待 jsch 内部处理
Threads.sleep(100);
this.closeStream();
Threads.sleep(100);
// 响应结果
if (ex == null) {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_FINISH, null);
} else {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, ex);
}
});
} }
@Override @Override
@@ -96,9 +81,72 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
this.closeStream(); this.closeStream();
} }
@Override
public void writeTo(OutputStream outputStream) {
String channelId = channel.getId();
Ref<Exception> ex = new Ref<>();
try {
byte[] buffer = new byte[Const.BUFFER_KB_32];
int len;
int i = 0;
int size = 0;
// 响应文件内容
while (this.inputStream != null && (len = this.inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
size += len;
// 不要每次都 flush 和 send > 1mb
if (i == 32) {
i = 0;
}
// 首次触发
if (i == 0) {
this.flushAndSendProgress(outputStream, size);
}
i++;
}
// 最后一次也要 flush
if (i != 0) {
this.flushAndSendProgress(outputStream, size);
}
log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path);
} catch (Exception e) {
log.error("DownloadSession.download error channelId: {}, path: {}", channelId, path, e);
ex.set(e);
}
// 异步关闭
AssetThreadPools.TERMINAL_OPERATOR.execute(() -> {
// 关闭等待 jsch 内部处理
Threads.sleep(100);
this.closeStream();
Threads.sleep(100);
// 响应结果
Exception e = ex.getValue();
if (e == null) {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_FINISH, null);
} else {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, e);
}
});
}
/**
* 刷流 & 发送进度
*
* @param outputStream outputStream
* @param size size
* @throws IOException IOException
*/
private void flushAndSendProgress(OutputStream outputStream, int size) throws IOException {
// flush
outputStream.flush();
// send
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_PROGRESS, null, e -> e.setCurrentSize(size));
}
@Override @Override
protected void closeStream() { protected void closeStream() {
// 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor // 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor
this.path = null;
Streams.close(this.executor); Streams.close(this.executor);
this.executor = null; this.executor = null;
this.inputStream = null; this.inputStream = null;

View File

@@ -12,12 +12,12 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
public interface IDownloadSession extends StreamingResponseBody { public interface IDownloadSession extends StreamingResponseBody {
/** /**
* 开始下载 * 初始化下载
* *
* @param path path * @param path path
* @param token token * @param token token
*/ */
void startDownload(String path, String token); void downloadInit(String path, String token);
/** /**
* 停止下载 * 停止下载

View File

@@ -6,6 +6,7 @@ import com.orion.visor.framework.common.constant.ErrorMessage;
import com.orion.visor.framework.websocket.core.utils.WebSockets; import com.orion.visor.framework.websocket.core.utils.WebSockets;
import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType; import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType;
import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperatorResponse; import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperatorResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -62,9 +63,10 @@ public class TransferUtils {
public static String getErrorMessage(Exception ex) { public static String getErrorMessage(Exception ex) {
if (ex == null) { if (ex == null) {
return null; return null;
} } else if (ex instanceof InvalidArgumentException) {
if (ex instanceof InvalidArgumentException) {
return ex.getMessage(); return ex.getMessage();
} else if (ex instanceof ClientAbortException) {
return ErrorMessage.CLIENT_ABORT;
} }
return ErrorMessage.OPERATE_ERROR; return ErrorMessage.OPERATE_ERROR;
} }

View File

@@ -2,26 +2,37 @@ package com.orion.visor.module.asset.service.impl;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.orion.lang.constant.StandardContentType;
import com.orion.lang.define.wrapper.DataGrid; import com.orion.lang.define.wrapper.DataGrid;
import com.orion.lang.utils.Arrays1; import com.orion.lang.utils.Arrays1;
import com.orion.lang.utils.Strings; import com.orion.lang.utils.Strings;
import com.orion.lang.utils.io.Files1;
import com.orion.visor.framework.biz.operator.log.core.utils.OperatorLogs; import com.orion.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import com.orion.visor.framework.common.constant.Const;
import com.orion.visor.framework.common.constant.ErrorMessage;
import com.orion.visor.framework.common.constant.ExtraFieldConst; import com.orion.visor.framework.common.constant.ExtraFieldConst;
import com.orion.visor.module.asset.convert.HostSftpLogConvert; import com.orion.visor.module.asset.convert.HostSftpLogConvert;
import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType; import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
import com.orion.visor.module.asset.entity.request.host.HostSftpLogQueryRequest; import com.orion.visor.module.asset.entity.request.host.HostSftpLogQueryRequest;
import com.orion.visor.module.asset.entity.vo.HostSftpLogVO; import com.orion.visor.module.asset.entity.vo.HostSftpLogVO;
import com.orion.visor.module.asset.service.HostSftpLogService; import com.orion.visor.module.asset.handler.host.transfer.handler.ITransferHandler;
import com.orion.visor.module.asset.handler.host.transfer.manager.HostTransferManager;
import com.orion.visor.module.asset.handler.host.transfer.session.IDownloadSession;
import com.orion.visor.module.asset.service.HostSftpService;
import com.orion.visor.module.infra.api.OperatorLogApi; import com.orion.visor.module.infra.api.OperatorLogApi;
import com.orion.visor.module.infra.entity.dto.operator.OperatorLogQueryDTO; import com.orion.visor.module.infra.entity.dto.operator.OperatorLogQueryDTO;
import com.orion.web.servlet.web.Servlets;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* SFTP 操作日志 服务实现类 * SFTP 操作 服务实现类
* *
* @author Jiahang Li * @author Jiahang Li
* @version 1.0.0 * @version 1.0.0
@@ -29,11 +40,14 @@ import java.util.List;
*/ */
@Slf4j @Slf4j
@Service @Service
public class HostSftpLogServiceImpl implements HostSftpLogService { public class HostSftpServiceImpl implements HostSftpService {
@Resource @Resource
private OperatorLogApi operatorLogApi; private OperatorLogApi operatorLogApi;
@Resource
private HostTransferManager hostTransferManager;
@Override @Override
public DataGrid<HostSftpLogVO> getHostSftpLogPage(HostSftpLogQueryRequest request) { public DataGrid<HostSftpLogVO> getHostSftpLogPage(HostSftpLogQueryRequest request) {
// 查询 // 查询
@@ -62,6 +76,25 @@ public class HostSftpLogServiceImpl implements HostSftpLogService {
return effect; return effect;
} }
@Override
public StreamingResponseBody downloadWithTransferToken(String channelId, String transferToken, HttpServletResponse response) {
// 获取会话
IDownloadSession session = Optional.ofNullable(channelId)
.map(hostTransferManager::getHandler)
.map(ITransferHandler::getTokenSessions)
.map(s -> s.remove(transferToken))
.orElse(null);
// 响应会话
if (session == null) {
Servlets.setContentType(response, StandardContentType.TEXT_HTML);
Servlets.setCharset(response, Const.UTF_8);
return outputStream -> outputStream.write(Strings.bytes(ErrorMessage.SESSION_ABSENT));
}
// 响应文件
Servlets.setAttachmentHeader(response, Files1.getFileName(session.getPath()));
return session;
}
/** /**
* 构建查询对象 * 构建查询对象
* *

View File

@@ -1,5 +1,6 @@
import type { DataGrid, Pagination } from '@/types/global'; import type { DataGrid, Pagination } from '@/types/global';
import type { TableData } from '@arco-design/web-vue/es/table/interface'; import type { TableData } from '@arco-design/web-vue/es/table/interface';
import { httpBaseUrl } from '@/utils/env';
import axios from 'axios'; import axios from 'axios';
import qs from 'query-string'; import qs from 'query-string';
@@ -46,17 +47,25 @@ export interface HostSftpLogExtra {
* SFTP * SFTP
*/ */
export function getHostSftpLogPage(request: HostSftpLogQueryRequest) { export function getHostSftpLogPage(request: HostSftpLogQueryRequest) {
return axios.post<DataGrid<HostSftpLogQueryResponse>>('/asset/host-sftp-log/query', request); return axios.post<DataGrid<HostSftpLogQueryResponse>>('/asset/host-sftp/query-log', request);
} }
/** /**
* SFTP * SFTP
*/ */
export function deleteHostSftpLog(idList: Array<number>) { export function deleteHostSftpLog(idList: Array<number>) {
return axios.delete('/asset/host-sftp-log/delete', { return axios.delete('/asset/host-sftp/delete-log', {
params: { idList }, params: { idList },
paramsSerializer: params => { paramsSerializer: params => {
return qs.stringify(params, { arrayFormat: 'comma' }); return qs.stringify(params, { arrayFormat: 'comma' });
} }
}); });
} }
/**
*
*/
export function downloadWithTransferToken(channelId: string, transferToken: string) {
window.open(`${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`, 'newWindow');
}

View File

@@ -172,9 +172,9 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import type { HostSftpLogQueryRequest, HostSftpLogQueryResponse } from '@/api/asset/host-sftp-log'; import type { HostSftpLogQueryRequest, HostSftpLogQueryResponse } from '@/api/asset/host-sftp';
import { reactive, ref, onMounted } from 'vue'; import { reactive, ref, onMounted } from 'vue';
import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp-log'; import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp';
import { sftpOperatorTypeKey, sftpOperatorResultKey, SftpOperatorType } from '../types/const'; import { sftpOperatorTypeKey, sftpOperatorResultKey, SftpOperatorType } from '../types/const';
import { usePagination, useRowSelection } from '@/types/table'; import { usePagination, useRowSelection } from '@/types/table';
import { useDictStore } from '@/store'; import { useDictStore } from '@/store';

View File

@@ -8,58 +8,53 @@ export default class SftpTransferDownloader implements ISftpTransferDownloader {
public abort: boolean; public abort: boolean;
private blobArr: Array<Blob>;
private client: WebSocket; private client: WebSocket;
private item: SftpTransferItem; private item: SftpTransferItem;
constructor(item: SftpTransferItem, client: WebSocket) { constructor(item: SftpTransferItem, client: WebSocket) {
this.abort = false; this.abort = false;
this.blobArr = [];
this.item = item; this.item = item;
this.client = client; this.client = client;
} }
// 开始下载 // 开始下载
startDownload() { initDownload() {
this.item.status = TransferStatus.TRANSFERRING; this.item.status = TransferStatus.TRANSFERRING;
// 发送开始下载信息 // 发送开始下载信息
this.client?.send(JSON.stringify({ this.client?.send(JSON.stringify({
type: TransferOperatorType.DOWNLOAD_START, type: TransferOperatorType.DOWNLOAD_INIT,
path: getPath(this.item.parentPath + '/' + this.item.name), path: getPath(this.item.parentPath + '/' + this.item.name),
hostId: this.item.hostId hostId: this.item.hostId
})); }));
} }
// 接收 blob
resolveBlob(blob: Blob) {
this.blobArr.push(blob);
this.item.currentSize += blob.size;
}
// 下载完成 // 下载完成
downloadFinish() { downloadFinish() {
if (this.abort) { if (this.abort) {
// 中断则不触发下载 // 中断则不触发下载
this.blobArr = [];
return; return;
} }
try { // 设置实际大小
// 触发下载 this.item.currentSize = this.item.totalSize;
saveAs(new Blob(this.blobArr, { if (this.item.totalSize === 0) {
type: 'application/octet-stream' // 空文件直接触发下载
}), getFileName(this.item.name)); try {
// 触发下载
saveAs(new Blob([], {
type: 'application/octet-stream'
}), getFileName(this.item.name));
this.item.status = TransferStatus.SUCCESS;
} catch (e) {
this.item.status = TransferStatus.ERROR;
this.item.errorMessage = '保存失败';
}
} else {
this.item.status = TransferStatus.SUCCESS; this.item.status = TransferStatus.SUCCESS;
} catch (e) {
this.item.status = TransferStatus.ERROR;
this.item.errorMessage = '保存失败';
} finally {
this.blobArr = [];
} }
} }
// 下载失败 // 下载失败
downloadError(msg: string | undefined) { downloadError(msg: string | undefined) {
this.blobArr = [];
this.item.status = TransferStatus.ERROR; this.item.status = TransferStatus.ERROR;
this.item.errorMessage = msg || '下载失败'; this.item.errorMessage = msg || '下载失败';
} }

View File

@@ -4,6 +4,7 @@ import { TransferReceiverType, TransferStatus, TransferType } from '../types/ter
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal'; import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal';
import { nextId } from '@/utils'; import { nextId } from '@/utils';
import { downloadWithTransferToken } from '@/api/asset/host-sftp';
import SftpTransferUploader from './sftp-transfer-uploader'; import SftpTransferUploader from './sftp-transfer-uploader';
import SftpTransferDownloader from './sftp-transfer-downloader'; import SftpTransferDownloader from './sftp-transfer-downloader';
@@ -144,7 +145,9 @@ export default class SftpTransferManager implements ISftpTransferManager {
// 计算传输进度 // 计算传输进度
private calcProgress() { private calcProgress() {
this.transferList.forEach(item => { this.transferList.forEach(item => {
item.progress = (item.currentSize / item.totalSize * 100).toFixed(2); if (item.totalSize != 0) {
item.progress = (item.currentSize / item.totalSize * 100).toFixed(2);
}
}); });
} }
@@ -165,7 +168,7 @@ export default class SftpTransferManager implements ISftpTransferManager {
this.uploadFile(); this.uploadFile();
} else { } else {
// 下载 // 下载
this.uploadDownload(); this.downloadFile();
} }
} else { } else {
// 无任务关闭会话 // 无任务关闭会话
@@ -175,27 +178,28 @@ export default class SftpTransferManager implements ISftpTransferManager {
// 接收消息 // 接收消息
private async resolveMessage(message: MessageEvent) { private async resolveMessage(message: MessageEvent) {
if (message.data instanceof Blob) { // 文本消息
// 二进制消息 下载数据 const data = JSON.parse(message.data) as TransferOperatorResponse;
this.resolveDownloadBlob(message.data); if (data.type === TransferReceiverType.NEXT_TRANSFER
} else { || data.type === TransferReceiverType.UPLOAD_FINISH
// 文本消息 || data.type === TransferReceiverType.UPLOAD_ERROR) {
const data = JSON.parse(message.data) as TransferOperatorResponse; // 执行下一个传输任务
if (data.type === TransferReceiverType.NEXT_TRANSFER this.resolveNextTransfer(data);
|| data.type === TransferReceiverType.UPLOAD_FINISH } else if (data.type === TransferReceiverType.UPLOAD_NEXT_BLOCK) {
|| data.type === TransferReceiverType.UPLOAD_ERROR) { // 接收下一块上传数据
// 执行下一个传输任务 await this.resolveUploadNextBlock();
this.resolveNextTransfer(data); } else if (data.type === TransferReceiverType.DOWNLOAD_START) {
} else if (data.type === TransferReceiverType.UPLOAD_NEXT_BLOCK) { // 开始下载
// 接收下一块上传数据 this.resolveDownloadStart(data);
await this.resolveUploadNextBlock(); } else if (data.type === TransferReceiverType.DOWNLOAD_PROGRESS) {
} else if (data.type === TransferReceiverType.DOWNLOAD_FINISH) { // 下载进度
// 下载完成 this.resolveDownloadProgress(data);
this.resolveDownloadFinish(); } else if (data.type === TransferReceiverType.DOWNLOAD_FINISH) {
} else if (data.type === TransferReceiverType.DOWNLOAD_ERROR) { // 下载完成
// 下载失败 this.resolveDownloadFinish();
this.resolveDownloadError(data.msg); } else if (data.type === TransferReceiverType.DOWNLOAD_ERROR) {
} // 下载失败
this.resolveDownloadError(data.msg);
} }
} }
@@ -208,11 +212,11 @@ export default class SftpTransferManager implements ISftpTransferManager {
} }
// 下载文件 // 下载文件
private uploadDownload() { private downloadFile() {
// 创建下载器 // 创建下载器
this.currentDownloader = new SftpTransferDownloader(this.currentItem as SftpTransferItem, this.client as WebSocket); this.currentDownloader = new SftpTransferDownloader(this.currentItem as SftpTransferItem, this.client as WebSocket);
// 开始下载 // 初始化下载
this.currentDownloader.startDownload(); this.currentDownloader.initDownload();
} }
// 接收下一个传输任务响应 // 接收下一个传输任务响应
@@ -251,9 +255,16 @@ export default class SftpTransferManager implements ISftpTransferManager {
} }
} }
// 接收下载数据 // 接收开始下载响应
private resolveDownloadBlob(blob: Blob) { private resolveDownloadStart(data: TransferOperatorResponse) {
this.currentDownloader?.resolveBlob(blob); downloadWithTransferToken(data.channelId as string, data.transferToken as string);
}
// 接收下载进度响应
private resolveDownloadProgress(data: TransferOperatorResponse) {
if (this.currentItem && data.currentSize) {
this.currentItem.currentSize = data.currentSize;
}
} }
// 接收下载完成响应 // 接收下载完成响应

View File

@@ -336,7 +336,7 @@ export const TransferOperatorType = {
UPLOAD_START: 'uploadStart', UPLOAD_START: 'uploadStart',
UPLOAD_FINISH: 'uploadFinish', UPLOAD_FINISH: 'uploadFinish',
UPLOAD_ERROR: 'uploadError', UPLOAD_ERROR: 'uploadError',
DOWNLOAD_START: 'downloadStart', DOWNLOAD_INIT: 'downloadInit',
DOWNLOAD_ABORT: 'downloadAbort', DOWNLOAD_ABORT: 'downloadAbort',
}; };
@@ -346,6 +346,8 @@ export const TransferReceiverType = {
UPLOAD_NEXT_BLOCK: 'uploadNextBlock', UPLOAD_NEXT_BLOCK: 'uploadNextBlock',
UPLOAD_FINISH: 'uploadFinish', UPLOAD_FINISH: 'uploadFinish',
UPLOAD_ERROR: 'uploadError', UPLOAD_ERROR: 'uploadError',
DOWNLOAD_START: 'downloadStart',
DOWNLOAD_PROGRESS: 'downloadProgress',
DOWNLOAD_FINISH: 'downloadFinish', DOWNLOAD_FINISH: 'downloadFinish',
DOWNLOAD_ERROR: 'downloadError', DOWNLOAD_ERROR: 'downloadError',
}; };

View File

@@ -432,10 +432,8 @@ export interface ISftpTransferUploader {
export interface ISftpTransferDownloader { export interface ISftpTransferDownloader {
// 是否中断 // 是否中断
abort: boolean; abort: boolean;
// 开始下载 // 初始化下载
startDownload: () => void; initDownload: () => void;
// 接收 blob
resolveBlob: (blob: Blob) => void;
// 下载完成 // 下载完成
downloadFinish: () => void; downloadFinish: () => void;
// 下载失败 // 下载失败
@@ -461,8 +459,11 @@ export interface SftpTransferItem {
// 传输操作响应 // 传输操作响应
export interface TransferOperatorResponse { export interface TransferOperatorResponse {
channelId?: string;
type: string; type: string;
hostId?: number; hostId?: number;
currentSize?: number;
transferToken?: string;
success: boolean; success: boolean;
msg?: string; msg?: string;
} }