✨ 修改下载文件逻辑.
This commit is contained in:
@@ -97,4 +97,6 @@ public interface ErrorMessage {
|
|||||||
|
|
||||||
String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置";
|
String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置";
|
||||||
|
|
||||||
|
String CLIENT_ABORT = "手动中断";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 中断下载
|
* 中断下载
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载失败
|
* 下载失败
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止下载
|
* 停止下载
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建查询对象
|
* 构建查询对象
|
||||||
*
|
*
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 || '下载失败';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收下载完成响应
|
// 接收下载完成响应
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user