🚀 feat(server): SSH 文件管理支持浏览器分片上传

This commit is contained in:
小吾立 2024-07-24 16:29:48 +08:00
parent 6cbbe721e8
commit 8e348ebcc9
9 changed files with 294 additions and 5 deletions

View File

@ -10,6 +10,7 @@
4. 【server】修复 页面未刷新情况下打开弹窗次数过多不能提示窗口层级太低(感谢[@lin_yeqi](https://gitee.com/lin_yeqi) [Gitee issues IAEBUZ](https://gitee.com/dromara/Jpom/issues/IAEBUZ)
5. 【server】优化 分发日志现在关联数据信息(感谢[@pumpkinor](https://gitee.com/pumpkinor) [Gitee issues IAF7IV](https://gitee.com/dromara/Jpom/issues/IAF7IV)
6. 【server】优化 分发文件使用文件中心或者静态文件上传至节点使用实际文件名(感谢[@pumpkinor](https://gitee.com/pumpkinor) [Gitee issues IAF7GD](https://gitee.com/dromara/Jpom/issues/IAF7GD)
7. 【server】优化 SSH 文件管理支持浏览器分片上传(感谢[@超人那个超i](https://gitee.com/chao_a) [Gitee issues IAD9W4](https://gitee.com/dromara/Jpom/issues/IAD9W4)
------

View File

@ -46,6 +46,7 @@ import org.dromara.jpom.util.CompressionFileUtil;
import org.dromara.jpom.util.StringUtil;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
@ -481,6 +482,53 @@ public abstract class BaseSshFileController extends BaseServerController {
return false;
}
/**
* 上传分片
*
* @param file 文件对象
* @param sliceId 分片id
* @param totalSlice 总分片
* @param nowSlice 当前分片
* @param fileSumMd5 文件 md5
* @return json
*/
@PostMapping(value = "upload-sharding", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.UPLOAD, log = false)
public IJsonMessage<String> uploadSharding(MultipartFile file,
String sliceId,
Integer totalSlice,
Integer nowSlice,
String fileSumMd5,
@ValidatorItem String id,
@ValidatorItem String allowPathParent,
@ValidatorItem String nextPath) {
return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineSshModel, itemConfig) -> {
String remotePath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath);
Session session = null;
ChannelSftp channel = null;
try {
session = sshService.getSessionByModel(machineSshModel);
channel = (ChannelSftp) JschUtil.openChannel(session, ChannelType.SFTP);
channel.cd(remotePath);
String originalFilename = file.getOriginalFilename();
// xxxx.txt.1
originalFilename = StrUtil.subBefore(originalFilename, ".", true);
if (nowSlice == 0) {
channel.put(file.getInputStream(), originalFilename, ChannelSftp.OVERWRITE);
} else {
channel.put(file.getInputStream(), originalFilename, ChannelSftp.APPEND);
}
} catch (Exception e) {
log.error(I18nMessageUtil.get("i18n.ssh_file_upload_exception.5c1c"), e);
return new JsonMessage<>(400, I18nMessageUtil.get("i18n.upload_failed.b019") + e.getMessage());
} finally {
JschUtil.close(channel);
JschUtil.close(session);
}
return JsonMessage.success(I18nMessageUtil.get("i18n.upload_success.a769"));
});
}
@RequestMapping(value = "upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.UPLOAD)

View File

@ -33,6 +33,19 @@ export function uploadFile(baseUrl, formData) {
})
}
export function uploadShardingFile(baseUrl, formData) {
return axios({
url: baseUrl + 'upload-sharding',
headers: {
'Content-Type': 'multipart/form-data;charset=UTF-8'
},
method: 'post',
// 0 表示无超时时间
timeout: 0,
data: formData
})
}
/**
*
* @param {String} id

View File

@ -606,6 +606,7 @@
"i18n_37c1eb9b23": "configuration file path",
"i18n_37f031338a": "Upload the compressed package and automatically decompress it.",
"i18n_37f1931729": "Data directory occupies space:",
"i18n_383952103d": "This shard upload is implemented using simple logic. When uploading the first shard, overwrite mode is used, and subsequent shards use append mode. If the file corresponding to the upload interruption is a incomplete file, it will not be able to be used normally.",
"i18n_384f337da1": "Synchronization mechanism is adopted",
"i18n_3867e350eb": "environment variables",
"i18n_386edb98a5": "Custom script projects (python, nodejs, go, interface exploration, es) [recommended]",
@ -1315,6 +1316,7 @@
"i18n_7760785daf": "free script",
"i18n_7764df7ccc": "Enabling differential releases but not empty releases is equivalent to only incremental and change updates",
"i18n_77688e95af": "Container rebuilding refers to recreating an identical container using the container parameters that have already been created.",
"i18n_776bf504a4": "Upload prompt",
"i18n_7777a83497": "Please enter build notes, length less than 240",
"i18n_77834eb6f5": "You use this system.",
"i18n_7785d9e038": "The node ID is not stored in the lower version of the project data, and the corresponding project data will also come out in the lonely data (such data does not affect the use).",
@ -2378,6 +2380,7 @@
"i18n_d61af4e686": "Breakpoint/sharding alias download",
"i18n_d61b8fde35": "Switch cluster",
"i18n_d64cf79bd4": "Are you sure you want to join the beta program?",
"i18n_d65551b090": "Fragmented file upload",
"i18n_d65d977f1d": "Fill in the operating parameters",
"i18n_d679aea3aa": "Running",
"i18n_d6937acda5": "The folder where the project is stored, the folder where the jar package is stored",
@ -2402,6 +2405,7 @@
"i18n_d82b19148f": "Please select the machine node to synchronize the authorization",
"i18n_d83aae15b5": "Online build files are mainly saved, warehouse files, build history products, etc. Active cleaning is not supported. If the file occupies too much, you can configure retention rules and whether to save warehouse, product files, etc. for a single build configuration",
"i18n_d84323ba8d": "The warehouse may exist repeatedly after automatic migration, please solve it manually.",
"i18n_d85279c536": "Do not refresh or close the browser window when uploading.",
"i18n_d87940854f": "number of plans",
"i18n_d87f215d9a": "Card",
"i18n_d88651584f": "free space",
@ -2466,6 +2470,7 @@
"i18n_dd23fdf796": "You can switch nodes during editing, but pay attention to whether the data matches.",
"i18n_dd4e55c39c": "Not started",
"i18n_dd95bf2d45": "normal login",
"i18n_dda8b4c10f": "Fragment upload",
"i18n_ddc7d28b7b": "variable",
"i18n_dddf944f5f": "Reset the page operation guide and navigation successfully.",
"i18n_ddf0c97bce": "Please pay attention to backing up your data to prevent data loss!!",

View File

@ -606,6 +606,7 @@
"i18n_37c1eb9b23": "配置文件路径",
"i18n_37f031338a": "上传压缩包并自动解压",
"i18n_37f1931729": "数据目录占用空间:",
"i18n_383952103d": "此分片上传是采用简单逻辑实现,当上传第一个分片时候使用覆盖模式,后续分片使用追加模式。如果上传中断对应的文件是一个非完整文件将无法正常使用。",
"i18n_384f337da1": "同步机制采用",
"i18n_3867e350eb": "环境变量",
"i18n_386edb98a5": "自定义脚本项目python、nodejs、go、接口探活、es【推荐】",
@ -1315,6 +1316,7 @@
"i18n_7760785daf": "自由脚本",
"i18n_7764df7ccc": "开启差异发布但不开启清空发布时相当于只做增量和变动更新",
"i18n_77688e95af": "容器重建是指使用已经创建的容器参数重新创建一个相同的容器。",
"i18n_776bf504a4": "上传提示",
"i18n_7777a83497": "请输入构建备注,长度小于 240",
"i18n_77834eb6f5": "您使用本系統",
"i18n_7785d9e038": "低版本项目数据未存储节点ID对应项目数据也将出来在孤独数据中此类数据不影响使用",
@ -2378,6 +2380,7 @@
"i18n_d61af4e686": "断点/分片别名下载",
"i18n_d61b8fde35": "切换集群",
"i18n_d64cf79bd4": "确认要加入 beta 计划吗?",
"i18n_d65551b090": "分片上传文件",
"i18n_d65d977f1d": "填写运行参数",
"i18n_d679aea3aa": "运行中",
"i18n_d6937acda5": "项目存储的文件夹jar 包存放的文件夹",
@ -2402,6 +2405,7 @@
"i18n_d82b19148f": "请选择要同步授权的机器节点",
"i18n_d83aae15b5": "在线构建文件主要保存,仓库文件,构建历史产物等。不支持主动清除,如果文件占用过大可以配置保留规则和对单个构建配置是否保存仓库、产物文件等",
"i18n_d84323ba8d": "仓库自动迁移后可能会重复存在请手动解决",
"i18n_d85279c536": "上传时请勿刷新或者关闭浏览器窗口。",
"i18n_d87940854f": "计划次数",
"i18n_d87f215d9a": "卡片",
"i18n_d88651584f": "剩余空间",
@ -2466,6 +2470,7 @@
"i18n_dd23fdf796": "编辑过程中可以切换节点但是要注意数据是否匹配",
"i18n_dd4e55c39c": "未开始",
"i18n_dd95bf2d45": "正常登录",
"i18n_dda8b4c10f": "分片上传",
"i18n_ddc7d28b7b": "变量",
"i18n_dddf944f5f": "重置页面操作引导、导航成功",
"i18n_ddf0c97bce": "请注意备份数据防止数据丢失!!",

View File

@ -606,6 +606,7 @@
"i18n_37c1eb9b23":"配置文件路徑",
"i18n_37f031338a":"上傳壓縮包並自動解壓",
"i18n_37f1931729":"數據目錄佔用空間:",
"i18n_383952103d":"此分片上傳是採用簡單邏輯實現,當上傳第一個分片時候使用覆蓋模式,後續分片使用追加模式。如果上傳中斷對應的文件是一個非完整文件將無法正常使用。",
"i18n_384f337da1":"同步機制採用",
"i18n_3867e350eb":"環境變量",
"i18n_386edb98a5":"自定義腳本項目python、nodejs、go、接口探活、es【推薦】",
@ -1315,6 +1316,7 @@
"i18n_7760785daf":"自由腳本",
"i18n_7764df7ccc":"開啟差異發佈但不開啟清空發佈時相當於只做增量和變動更新",
"i18n_77688e95af":"容器重建是指使用已經創建的容器參數重新創建一個相同的容器。",
"i18n_776bf504a4":"上傳提示",
"i18n_7777a83497":"請輸入構建備註,長度小於 240",
"i18n_77834eb6f5":"您使用本系統",
"i18n_7785d9e038":"低版本項目數據未存儲節點ID對應項目數據也將出來在孤獨數據中此類數據不影響使用",
@ -2378,6 +2380,7 @@
"i18n_d61af4e686":"斷點/分片別名下載",
"i18n_d61b8fde35":"切換集羣",
"i18n_d64cf79bd4":"確認要加入 beta 計劃嗎?",
"i18n_d65551b090":"分片上傳文件",
"i18n_d65d977f1d":"填寫運行參數",
"i18n_d679aea3aa":"運行中",
"i18n_d6937acda5":"項目存儲的文件夾jar 包存放的文件夾",
@ -2402,6 +2405,7 @@
"i18n_d82b19148f":"請選擇要同步授權的機器節點",
"i18n_d83aae15b5":"在線構建文件主要保存,倉庫文件,構建歷史產物等。不支持主動清除,如果文件佔用過大可以配置保留規則和對單個構建配置是否保存倉庫、產物文件等",
"i18n_d84323ba8d":"倉庫自動遷移後可能會重複存在請手動解決",
"i18n_d85279c536":"上傳時請勿刷新或者關閉瀏覽器窗口。",
"i18n_d87940854f":"計劃次數",
"i18n_d87f215d9a":"卡片",
"i18n_d88651584f":"剩餘空間",
@ -2466,6 +2470,7 @@
"i18n_dd23fdf796":"編輯過程中可以切換節點但是要注意數據是否匹配",
"i18n_dd4e55c39c":"未開始",
"i18n_dd95bf2d45":"正常登錄",
"i18n_dda8b4c10f":"分片上傳",
"i18n_ddc7d28b7b":"變量",
"i18n_dddf944f5f":"重置頁面操作引導、導航成功",
"i18n_ddf0c97bce":"請注意備份數據防止數據丟失!!",

View File

@ -606,6 +606,7 @@
"i18n_37c1eb9b23":"配置檔案路徑",
"i18n_37f031338a":"上傳壓縮包並自動解壓",
"i18n_37f1931729":"資料目錄佔用空間:",
"i18n_383952103d":"此分片上傳是採用簡單邏輯實現,當上傳第一個分片時候使用覆蓋模式,後續分片使用追加模式。如果上傳中斷對應的檔案是一個非完整檔案將無法正常使用。",
"i18n_384f337da1":"同步機制採用",
"i18n_3867e350eb":"環境變數",
"i18n_386edb98a5":"自定義指令碼專案python、nodejs、go、介面探活、es【推薦】",
@ -1315,6 +1316,7 @@
"i18n_7760785daf":"自由指令碼",
"i18n_7764df7ccc":"開啟差異釋出但不開啟清空釋出時相當於只做增量和變動更新",
"i18n_77688e95af":"容器重建是指使用已經建立的容器引數重新建立一個相同的容器。",
"i18n_776bf504a4":"上傳提示",
"i18n_7777a83497":"請輸入構建備註,長度小於 240",
"i18n_77834eb6f5":"您使用本系統",
"i18n_7785d9e038":"低版本專案資料未儲存節點ID對應專案資料也將出來在孤獨資料中此類資料不影響使用",
@ -2378,6 +2380,7 @@
"i18n_d61af4e686":"斷點/分片別名下載",
"i18n_d61b8fde35":"切換叢集",
"i18n_d64cf79bd4":"確認要加入 beta 計劃嗎?",
"i18n_d65551b090":"分片上傳檔案",
"i18n_d65d977f1d":"填寫執行引數",
"i18n_d679aea3aa":"執行中",
"i18n_d6937acda5":"專案儲存的資料夾jar 包存放的資料夾",
@ -2402,6 +2405,7 @@
"i18n_d82b19148f":"請選擇要同步授權的機器節點",
"i18n_d83aae15b5":"線上構建檔案主要儲存,倉庫檔案,構建歷史產物等。不支援主動清除,如果檔案佔用過大可以配置保留規則和對單個構建配置是否儲存倉庫、產物檔案等",
"i18n_d84323ba8d":"倉庫自動遷移後可能會重複存在請手動解決",
"i18n_d85279c536":"上傳時請勿重新整理或者關閉瀏覽器視窗。",
"i18n_d87940854f":"計劃次數",
"i18n_d87f215d9a":"卡片",
"i18n_d88651584f":"剩餘空間",
@ -2466,6 +2470,7 @@
"i18n_dd23fdf796":"編輯過程中可以切換節點但是要注意資料是否匹配",
"i18n_dd4e55c39c":"未開始",
"i18n_dd95bf2d45":"正常登入",
"i18n_dda8b4c10f":"分片上傳",
"i18n_ddc7d28b7b":"變數",
"i18n_dddf944f5f":"重置頁面操作引導、導航成功",
"i18n_ddf0c97bce":"請注意備份資料防止資料丟失!!",

View File

@ -48,6 +48,7 @@
<a-empty v-if="treeList.length === 0" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-directory-tree
v-model:selectedKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
:tree-data="treeList"
:field-names="replaceFields"
@select="onSelect"
@ -85,6 +86,13 @@
</a-menu>
</template>
</a-dropdown>
<a-button
size="small"
:disabled="!tempNode.nextPath"
type="primary"
@click="uploadShardingFileVisible = true"
>{{ $t('i18n_dda8b4c10f') }}</a-button
>
<a-dropdown :disabled="!tempNode.nextPath">
<a-button size="small" type="primary" @click="(e) => e.preventDefault()">{{
$t('i18n_26bb841878')
@ -226,6 +234,78 @@
>{{ $t('i18n_020f1ecd62') }}</a-button
>
</CustomModal>
<!-- 分片上传 -->
<CustomModal
v-if="uploadShardingFileVisible"
v-model:open="uploadShardingFileVisible"
destroy-on-close
:confirm-loading="confirmLoading"
:closable="!confirmLoading"
:keyboard="false"
width="35vw"
:title="$t('i18n_d65551b090')"
:footer="null"
:mask-closable="false"
>
<a-space direction="vertical" size="large" style="width: 100%">
<a-alert :message="$t('i18n_776bf504a4')" type="warning">
<template #description>
<ul>
<li>
{{ $t('i18n_383952103d') }}
</li>
<li>{{ $t('i18n_d85279c536') }}</li>
</ul>
</template>
</a-alert>
<a-upload
:file-list="uploadFileList"
:before-upload="
(file) => {
uploadFileList = [file]
return false
}
"
multiple
:disabled="!!percentage"
@remove="
(file) => {
const index = uploadFileList.indexOf(file)
//const newFileList = this.uploadFileList.slice();
uploadFileList.splice(index, 1)
return true
}
"
>
<template v-if="percentage">
<LoadingOutlined v-if="uploadFileList.length" />
<span v-else>-</span>
</template>
<a-button v-else><UploadOutlined />{{ $t('i18n_fd7e0c997d') }}</a-button>
</a-upload>
<a-row v-if="percentage">
<a-col span="24">
<a-progress :percent="percentage" class="max-progress">
<template #format="percent">
{{ percent }}%<template v-if="percentageInfo.total">
({{ renderSize(percentageInfo.total) }})
</template>
<template v-if="percentageInfo.duration">
{{ $t('i18n_833249fb92') }}:{{ formatDuration(percentageInfo.duration) }}
</template>
</template>
</a-progress>
</a-col>
</a-row>
<a-button type="primary" :disabled="fileUploadDisabled" @click="startUploadSharding">{{
$t('i18n_020f1ecd62')
}}</a-button>
</a-space>
</CustomModal>
<!-- 新增文件 目录 -->
<CustomModal
v-if="addFileFolderVisible"
@ -251,7 +331,7 @@
</a-row>
</a-space>
</CustomModal>
<!-- 编辑文件 -->
<CustomModal
v-if="editFileVisible"
v-model:open="editFileVisible"
@ -391,16 +471,19 @@ import {
uploadFile,
parsePermissions,
calcFilePermissionValue,
changeFilePermission
changeFilePermission,
uploadShardingFile
} from '@/api/ssh-file'
import codeEditor from '@/components/codeEditor'
import { ZIP_ACCEPT, renderSize, parseTime } from '@/utils/const'
import { ZIP_ACCEPT, renderSize, parseTime, concurrentExecution, formatDuration } from '@/utils/const'
import { Empty } from 'ant-design-vue'
import { uploadPieces } from '@/utils/upload-pieces'
export default {
components: {
codeEditor
},
inject: ['globalLoading'],
props: {
sshId: {
type: String,
@ -504,10 +587,17 @@ export default {
asc: true
},
confirmLoading: false,
selectedKeys: []
selectedKeys: [],
expandedKeys: [],
uploadShardingFileVisible: false,
percentage: 0,
percentageInfo: {}
}
},
computed: {
fileUploadDisabled() {
return this.uploadFileList.length === 0 || this.confirmLoading
},
nowPath() {
if (!this.tempNode.allowPathParent) {
return ''
@ -537,6 +627,7 @@ export default {
this.loadData()
},
methods: {
formatDuration,
changeSort(key, asc) {
this.sortMethod = { key: key, asc: asc }
localStorage.setItem('ssh-list-sort', JSON.stringify(this.sortMethod))
@ -822,7 +913,116 @@ export default {
})
})
},
startUploadSharding() {
//
this.confirmLoading = true
//
concurrentExecution(
this.uploadFileList.map((item, index) => {
// console.log(item);
return index
}),
// 1
1,
(curItem) => {
const file = this.uploadFileList[curItem]
this.uploadFileList = this.uploadFileList.map((fileItem, fileIndex) => {
if (fileIndex === curItem) {
fileItem.status = 'uploading'
}
return fileItem
})
this.percentage = 0
this.percentageInfo = {}
return new Promise((resolve, reject) => {
uploadPieces({
file,
resolveFileProcess: (msg) => {
this.globalLoading({
spinning: true,
tip: msg
})
},
resolveFileEnd: () => {
this.globalLoading(false)
},
process: (process, end, total, duration) => {
this.percentage = Math.max(this.percentage, process)
this.percentageInfo = { end, total, duration }
},
success: () => {
//
$notification.success({
message: this.$t('i18n_a7699ba731')
})
this.uploadFileList = this.uploadFileList.map((fileItem, fileIndex) => {
if (fileIndex === curItem) {
fileItem.status = 'done'
}
return fileItem
})
resolve()
},
error: (msg) => {
this.uploadFileList = this.uploadFileList.map((fileItem, fileIndex) => {
if (fileIndex === curItem) {
fileItem.status = 'error'
}
return fileItem
})
$notification.error({
message: msg
})
this.confirmLoading = false
reject()
},
uploadCallback: (formData) => {
return new Promise((resolve, reject) => {
formData.append('id', this.reqDataId)
formData.append('allowPathParent', this.tempNode.allowPathParent)
formData.append('unzip', this.uploadFileZip)
formData.append('nextPath', this.tempNode.nextPath)
//
uploadShardingFile(this.baseUrl, formData)
.then((res) => {
if (res.code === 200) {
resolve()
} else {
reject()
}
})
.catch(() => {
reject()
})
})
}
})
})
}
)
.then(() => {
//this.uploading = this.successSize !== this.uploadFileList.length
// //
// if (!this.uploading) {
// this.uploadFileList = []
// setTimeout(() => {
// this.loadFileList()
// this.uploadFileVisible = false
// }, 2000)
// }
this.percentage = 0
this.percentageInfo = {}
this.uploadFileList = []
this.loadFileList()
this.uploadShardingFileVisible = false
})
.finally(() => {
this.confirmLoading = false
//
})
},
//
handleEdit(record) {
this.temp = Object.assign({}, record)
@ -1005,6 +1205,9 @@ export default {
}
</script>
<style scoped>
:deep(.ant-progress-text) {
width: auto;
}
.ssh-file-layout {
padding: 0;
min-height: calc(100vh - 75px);

View File

@ -11,6 +11,8 @@
>
<a-directory-tree
v-if="treeList.length"
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
multiple
default-expand-all
:tree-data="treeList"
@ -89,7 +91,9 @@ export default {
children: 'children',
title: 'name',
key: 'id'
}
},
expandedKeys: [],
selectedKeys: []
}
},
computed: {},