feat: 新增统一文件模块,统一文件操作

This commit is contained in:
meilin.huang
2024-10-21 22:27:42 +08:00
parent 6343173cf8
commit ea3c70a8a8
71 changed files with 1642 additions and 1216 deletions

View File

@@ -18,7 +18,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.8.5",
"element-plus": "^2.8.6",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
@@ -57,7 +57,7 @@
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.28.0",
"prettier": "^3.2.5",
"sass": "^1.79.5",
"sass": "^1.80.3",
"typescript": "^5.6.3",
"vite": "^5.4.9",
"vue-eslint-parser": "^9.4.3"

View File

@@ -14,4 +14,5 @@ export default {
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
};

View File

@@ -209,6 +209,36 @@ export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;
}
/**
* 获取文件url地址
* @param key 文件key
* @returns 文件url
*/
export function getFileUrl(key: string) {
return `${baseUrl}/sys/files/${key}`;
}
/**
* 获取系统文件上传url
* @param key 文件key
* @returns 文件上传url
*/
export function getUploadFileUrl(key: string = '') {
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
}
/**
* 下载文件
* @param key 文件key
*/
export function downloadFile(key: string) {
const a = document.createElement('a');
a.setAttribute('href', `${getFileUrl(key)}`);
a.setAttribute('target', '_blank');
a.click();
a.remove();
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;

View File

@@ -0,0 +1,60 @@
<template>
<el-tooltip :content="formatByteSize(fileDetail.size)" placement="left">
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
</el-tooltip>
{{ fileDetail.filename }}
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
fileKey: {
type: String,
required: true,
},
files: {
type: [Array],
},
canDownload: {
type: Boolean,
default: true,
},
});
onMounted(async () => {
setFileInfo();
});
watch(
() => props.fileKey,
async (val) => {
if (val) {
setFileInfo();
}
}
);
const fileDetail: any = ref({});
const setFileInfo = async () => {
if (!props.fileKey) {
return;
}
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -1 +1 @@
@import 'common/transition.scss';
@use 'common/transition.scss';

View File

@@ -1,4 +1,4 @@
@import 'mixins/index.scss';
@use 'mixins/index' as mixins;
/* Button 按钮
------------------------------- */
@@ -97,7 +97,7 @@
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
@include generalIcon;
@include mixins.generalIcon;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色

View File

@@ -1,8 +1,8 @@
@import './app.scss';
@import './base.scss';
@import './other.scss';
@import './element.scss';
@import './media/media.scss';
@import './waves.scss';
@import './dark.scss';
@import './iconSelector.scss';
@use './app.scss';
@use './base.scss';
@use './other.scss';
@use './element.scss';
@use './media/media.scss';
@use './waves.scss';
@use './dark.scss';
@use './iconSelector.scss';

View File

@@ -1,45 +1,54 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
.big-data-down-left {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
}
}
.big-data-down-center {
width: 100% !important;
.big-data-down-center-one,
.big-data-down-center-two {
min-height: 196.24px;
padding-left: 15px !important;
.big-data-down-center-one-content {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
.flex-warp-item-box {
@extend .big-data-down-center-one-content;
}
}
}
.big-data-down-right {
.flex-warp-item {
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
&:nth-of-type(2) {
padding-left: 15px !important;
}
&:last-of-type {
.flex-warp-item-box {
border: none !important;
@@ -51,17 +60,20 @@
/* 页面宽度大于768px小于1200px
------------------------------- */
@media screen and (min-width: $sm) and (max-width: $lg) {
@media screen and (min-width: index.$sm) and (max-width: index.$lg) {
.chart-warp-bottom {
.big-data-down-left {
width: 50% !important;
}
.big-data-down-center {
width: 50% !important;
}
.big-data-down-right {
.flex-warp-item {
width: 50% !important;
&:nth-of-type(2) {
padding-left: 7.5px !important;
}
@@ -72,19 +84,22 @@
/* 页面宽度小于1200px
------------------------------- */
@media screen and (max-width: $lg) {
@media screen and (max-width: index.$lg) {
.chart-warp-top {
.up-left {
display: none;
}
}
.chart-warp-bottom {
overflow-y: auto !important;
flex-wrap: wrap;
.big-data-down-right {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;

View File

@@ -1,8 +1,8 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
@media screen and (max-width: index.$xs) {
.el-cascader__dropdown.el-popper {
overflow: auto;
max-width: 100%;

View File

@@ -1,4 +1,4 @@
@import './index.scss';
@use './index.scss';
/* 页面宽度小于800px
------------------------------- */
@@ -6,6 +6,7 @@
.el-dialog {
width: 90% !important;
}
.el-dialog.is-fullscreen {
width: 100% !important;
}

View File

@@ -1,22 +1,25 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
.error {
.error-flex {
flex-direction: column-reverse !important;
height: auto !important;
width: 100% !important;
}
.right,
.left {
flex: unset !important;
display: flex !important;
}
.left-item {
margin: auto !important;
}
.right img {
max-width: 450px !important;
@extend .left-item;
@@ -26,7 +29,7 @@
/* 页面宽度大于768px小于992px
------------------------------- */
@media screen and (min-width: $sm) and (max-width: $md) {
@media screen and (min-width: index.$sm) and (max-width: index.$md) {
.error {
.error-flex {
padding-left: 30px !important;

View File

@@ -1,12 +1,13 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
@media screen and (max-width: index.$xs) {
.el-form-item__label {
width: 100% !important;
text-align: left !important;
}
.el-form-item__content {
margin-left: 0 !important;
}

View File

@@ -1,8 +1,9 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
.home-warning-media,
.home-dynamic-media {
margin-top: 15px;

View File

@@ -1,8 +1,9 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
@media screen and (max-width: index.$xs) {
// MessageBox 弹框
.el-message-box {
width: 80% !important;
@@ -11,11 +12,13 @@
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
// Breadcrumb 面包屑
.layout-navbars-breadcrumb-hide {
display: none;
}
// 外链视图
.layout-view-link {
a {
@@ -23,6 +26,7 @@
text-align: center;
}
}
// 菜单搜索
.layout-search-dialog {
.el-autocomplete {
@@ -34,9 +38,11 @@
/* 页面宽度小于1000px
------------------------------- */
@media screen and (max-width: 1000px) {
// 布局配置
.layout-drawer-content-flex {
position: relative;
&::after {
content: '手机版不支持切换布局';
position: absolute;

View File

@@ -1,17 +1,19 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
@media screen and (max-width: index.$xs) {
.login-container {
.login-content {
width: 90% !important;
padding: 20px 0 !important;
}
.login-content-form-btn {
width: 100% !important;
padding: 12px 0 !important;
}
.login-copyright {
.login-copyright-msg {
white-space: unset !important;

View File

@@ -1,12 +1,12 @@
@import './login.scss';
@import './error.scss';
@import './layout.scss';
@import './personal.scss';
@import './tagsView.scss';
@import './home.scss';
@import './chart.scss';
@import './form.scss';
@import './scrollbar.scss';
@import './pagination.scss';
@import './dialog.scss';
@import './cityLinkage.scss';
@use './login.scss';
@use './error.scss';
@use './layout.scss';
@use './personal.scss';
@use './tagsView.scss';
@use './home.scss';
@use './chart.scss';
@use './form.scss';
@use './scrollbar.scss';
@use './pagination.scss';
@use './dialog.scss';
@use './cityLinkage.scss';

View File

@@ -1,8 +1,9 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
@media screen and (max-width: index.$xs) {
.el-pager,
.el-pagination__jump {
display: none !important;

View File

@@ -1,14 +1,16 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
.personal-info {
padding-left: 0 !important;
margin-top: 15px;
}
.personal-recommend-col {
margin-bottom: 15px;
&:last-of-type {
margin-bottom: 0;
}

View File

@@ -1,16 +1,19 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
// 滚动条的宽度
::-webkit-scrollbar {
width: 3px !important;
height: 3px !important;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
@@ -19,13 +22,16 @@
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
// element plus scrollbar
.el-scrollbar__bar.is-vertical {
width: 2px !important;
}
.el-scrollbar__bar.is-horizontal {
height: 2px !important;
}
@@ -34,14 +40,17 @@
/* 页面宽度大于768px
------------------------------- */
@media screen and (min-width: 769px) {
// 滚动条的宽度
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
@@ -50,6 +59,7 @@
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}

View File

@@ -1,8 +1,8 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
@media screen and (max-width: index.$sm) {
.tags-view-form {
.tags-view-form-col {
margin-bottom: 20px;

View File

@@ -35,21 +35,25 @@
// @include scrollBar;
// }
@mixin scrollBar {
// 滚动条凹槽的颜色,还可以设置边框属性
&::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
}
// 滚动条的宽度
&::-webkit-scrollbar {
width: 9px;
height: 9px;
}
// 滚动条的设置
&::-webkit-scrollbar-thumb {
background-color: #dddddd;
background-clip: padding-box;
min-height: 28px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}

View File

@@ -7,6 +7,7 @@
border-top-right-radius: 3px;
z-index: 2 !important;
}
.w-e-text-container {
border: 1px solid #ebeef5 !important;
border-top: none !important;
@@ -20,9 +21,11 @@
#screenShotContainer {
z-index: 9998 !important;
}
#toolPanel {
height: 42px !important;
}
#optionPanel {
height: 37px !important;
}

View File

@@ -6,7 +6,15 @@
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<el-upload
class="h100 personal-user-left-upload"
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
:limit="1"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:on-success="handleAvatarSuccess"
accept=".png,.jpg,.jpeg"
>
<img :src="userInfo.photo" />
</el-upload>
</div>
@@ -81,8 +89,7 @@
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small"
empty-text="暂无操作记录">
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
@@ -109,16 +116,14 @@
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('db')">
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon"
:color="TagResourceTypeEnum.Db.extra.iconColor"/>
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
<span class="">{{ state.db.num }}</span>
</div>
</el-row>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small"
empty-text="暂无操作记录">
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
@@ -159,8 +164,7 @@
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small"
empty-text="暂无操作记录">
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
@@ -199,8 +203,7 @@
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small"
empty-text="暂无操作记录">
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
@@ -268,6 +271,9 @@ import {resourceOpLogApi} from '../ops/tag/api';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { getAllTagInfoByCodePaths } from '../ops/component/tag';
import { ElMessage } from 'element-plus';
import { getFileUrl, getUploadFileUrl } from '@/common/request';
import { saveUser } from '@/common/utils/storage';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
@@ -363,6 +369,23 @@ const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.size >= 512 * 1024) {
ElMessage.error('头像不能超过512KB!');
return false;
}
return true;
};
const handleAvatarSuccess = (response: any, uploadFile: any) => {
userInfo.value.photo = URL.createObjectURL(uploadFile.raw);
const newUser = { ...userInfo.value };
newUser.photo = getFileUrl(`avatar_${userInfo.value.username}`);
// 存储用户信息到浏览器缓存
saveUser(newUser);
};
// 初始化数字滚动
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
@@ -448,7 +471,7 @@ const toPage = (item: any, codePath = '') => {
</script>
<style scoped lang="scss">
@import '@/theme/mixins/index.scss';
@use '@/theme/mixins/index.scss' as mixins;
.personal {
.personal-user {
@@ -486,7 +509,7 @@ const toPage = (item: any, codePath = '') => {
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
.personal-item {
@@ -496,11 +519,11 @@ const toPage = (item: any, codePath = '') => {
.personal-item-label {
color: gray;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
}
}
@@ -531,7 +554,7 @@ const toPage = (item: any, codePath = '') => {
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
color: grey;
text-decoration: none;
}

View File

@@ -144,6 +144,7 @@ import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
import { getToken } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
import { getFileUrl } from '@/common/request';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -347,19 +348,27 @@ const updateUserInfo = async () => {
}
};
const loginResDeal = (loginRes: any) => {
const loginResDeal = async (loginRes: any) => {
state.loginRes = loginRes;
// 用户信息
const userInfos = {
name: loginRes.name,
username: loginRes.username,
// 头像
photo: letterAvatar(loginRes.username),
time: new Date().getTime(),
lastLoginTime: loginRes.lastLoginTime,
lastLoginIp: loginRes.lastLoginIp,
photo: '',
};
const avatarFileKey = `avatar_${loginRes.username}`;
const avatarFileDetail = await openApi.getFileDetail([avatarFileKey]);
// 说明存在头像文件
if (avatarFileDetail.length > 0) {
userInfos.photo = getFileUrl(avatarFileKey);
} else {
userInfos.photo = letterAvatar(loginRes.username);
}
// 存储用户信息到浏览器缓存
saveUser(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)

View File

@@ -81,7 +81,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
<el-dropdown-item
<!-- <el-dropdown-item
:command="{ type: 'backupDb', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
@@ -98,7 +98,7 @@
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>

View File

@@ -1,6 +1,6 @@
<template>
<div class="db-transfer-file">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="900px">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="1000px">
<page-table
ref="pageTableRef"
:data="state.tableData"
@@ -25,6 +25,10 @@
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
</template>
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
</template>
<template #fileDbType="{ data }">
<span>
<SvgIcon :name="getDbDialect(data.fileDbType).getInfo().icon" :size="18" />
@@ -32,37 +36,16 @@
</span>
</template>
<template #status="{ data }">
<span>
<el-tag v-if="data.status == 1" class="ml-2" type="primary">执行中</el-tag>
<el-tag v-else-if="data.status == 2" class="ml-2" type="success">成功</el-tag>
<el-tag v-else-if="data.status == -1" class="ml-2" type="danger">失败</el-tag>
</span>
</template>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.run] && data.status === 2" @click="openRun(data)" type="primary" link>执行</el-button>
<el-button v-if="actionBtns[perms.rename] && data.status === 2" @click="rename(data)" type="warning" link>重命名</el-button>
<el-button v-if="actionBtns[perms.down] && data.status === 2" @click="down(data)" type="primary" link>下载</el-button>
<el-button v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value" @click="openRun(data)" type="primary" link
>执行</el-button
>
<el-button v-if="data.logId" @click="openLog(data)" type="success" link>日志</el-button>
</template>
</page-table>
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
</el-dialog>
<el-dialog :title="state.renameDialog.title" v-model="state.renameDialog.visible" :destroy-on-close="true" width="400px">
<el-form :model="state.renameDialog.renameForm" ref="renameFormRef" label-width="auto">
<el-form-item label="文件名" prop="fileName" required>
<el-input v-model="state.renameDialog.renameForm.fileName" placeholder="请输入文件名" />
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="state.renameDialog.cancel()">取 消</el-button>
<el-button type="primary" :loading="state.renameDialog.loading" @click="state.renameDialog.btnOk">确 定</el-button>
</div>
</template>
</el-dialog>
<el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px">
<el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
<el-form-item label="文件数据库类型" prop="dbType">
@@ -100,10 +83,10 @@ import { TableColumn } from '@/components/pagetable';
import { ElMessage, ElMessageBox } from 'element-plus';
import { hasPerms } from '@/components/auth/auth';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue';
import { DbTransferFileStatusEnum } from './enums';
const props = defineProps({
data: {
type: [Object],
@@ -116,22 +99,21 @@ const props = defineProps({
const dialogVisible = defineModel<boolean>('visible', { default: false });
const columns = ref([
TableColumn.new('fileName', '文件').setMinWidth(150),
TableColumn.new('createTime', '创建时间').setMinWidth(180).isTime(),
TableColumn.new('fileKey', '文件').setMinWidth(280).isSlot(),
TableColumn.new('createTime', '执行时间').setMinWidth(180).isTime(),
TableColumn.new('fileDbType', 'sql语言').setMinWidth(90).isSlot(),
TableColumn.new('status', '状态').isSlot(),
TableColumn.new('status', '状态').typeTag(DbTransferFileStatusEnum),
]);
const perms = {
del: 'db:transfer:files:del',
down: 'db:transfer:files:down',
rename: 'db:transfer:files:rename',
run: 'db:transfer:files:run',
};
const actionBtns = hasPerms([perms.del, perms.down, perms.rename, perms.run]);
const actionBtns = hasPerms([perms.del, perms.down, perms.run]);
const actionWidth = ((actionBtns[perms.rename] ? 1 : 0) + (actionBtns[perms.down] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0) + 1) * 55;
const actionWidth = ((actionBtns[perms.run] ? 1 : 0) + 1) * 55;
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
@@ -141,7 +123,6 @@ onMounted(async () => {
}
});
const renameFormRef: any = ref(null);
const runFormRef: any = ref(null);
const state = reactive({
@@ -210,31 +191,6 @@ const state = reactive({
}
},
},
renameDialog: {
visible: false,
title: '文件重命名',
renameForm: {
id: 0,
fileName: '',
},
loading: false,
cancel: function () {
state.renameDialog.visible = false;
state.renameDialog.renameForm = { id: 0, fileName: '' };
},
btnOk: function () {
renameFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
await dbApi.dbTransferFileRename.request(state.renameDialog.renameForm);
ElMessage.success('保存成功');
state.renameDialog.cancel();
await search();
});
},
},
selectionData: [], // 选中的数据
tableData: [],
});
@@ -262,18 +218,6 @@ const del = async function () {
}
};
const down = function (data: any) {
const a = document.createElement('a');
a.setAttribute('target', '_blank');
a.setAttribute('href', `${config.baseApiUrl}/dbTransfer/files/down/${data.fileUuid}?${joinClientParams()}`);
a.click();
a.remove();
};
const rename = function (data: any) {
state.renameDialog.visible = true;
const { id, fileName } = data;
state.renameDialog.renameForm = { id, fileName };
};
const openLog = function (data: any) {
state.logsDialog.logId = data.logId;
state.logsDialog.visible = true;

View File

@@ -80,7 +80,6 @@ export const dbApi = {
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRename: Api.newPost('/dbTransfer/files/rename'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
};

View File

@@ -160,7 +160,7 @@ const props = defineProps({
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
let dbDialect = computed(() => getDbDialect(props.dbType!, props.version));
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
type ColName = {
prop: string;

View File

@@ -43,3 +43,9 @@ export const DbTransferRunningStateEnum = {
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
Stop: EnumValue.of(-2, '手动终止').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, '执行中').setTagType('primary'),
Success: EnumValue.of(2, '成功').setTagType('success'),
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
};

View File

@@ -10,6 +10,10 @@
@open="getTermOps()"
>
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" />
</template>
<template #action="{ data }">
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
<el-button @click="showExecCmds(data)" type="primary" link>命令</el-button>
@@ -49,6 +53,8 @@ import 'asciinema-player/dist/bundle/asciinema-player.css';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { formatDate } from '@/common/utils/format';
import { getFileUrl } from '@/common/request';
import FileInfo from '@/components/file/FileInfo.vue';
const props = defineProps({
visible: { type: Boolean },
@@ -62,7 +68,7 @@ const columns = [
TableColumn.new('creator', '操作者').setMinWidth(120),
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
TableColumn.new('fileKey', '文件').isSlot(),
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
];
@@ -109,14 +115,9 @@ const playRec = async (rec: any) => {
player.dispose();
}
rec.playRecLoding = true;
const content = await machineApi.termOpRec.request({
recId: rec.id,
id: rec.machineId,
});
state.playerDialogVisible = true;
nextTick(() => {
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
player = AsciinemaPlayer.create(getFileUrl(rec.fileKey), playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,

View File

@@ -47,8 +47,6 @@ export const machineApi = {
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
// 机器终端操作记录列表
termOpRecs: Api.newGet('/machines/{machineId}/term-recs'),
// 机器终端操作记录详情
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
};
export const cronJobApi = {

View File

@@ -118,7 +118,7 @@ const unbindOAuth2 = async () => {
</script>
<style scoped lang="scss">
@import '../../theme/mixins/index.scss';
@use '@/theme/mixins/index.scss' as mixins;
.personal {
.personal-edit {
.personal-edit-title {
@@ -159,7 +159,7 @@ const unbindOAuth2 = async () => {
.personal-edit-safe-item-left-value {
color: gray;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
margin-right: 15px;
}
}

View File

@@ -81,6 +81,11 @@ const viteConfig: UserConfig = {
},
],
},
preprocessorOptions: {
scss: {
api: 'modern-compiler', // or 'modern'
},
},
},
};

View File

@@ -25,7 +25,7 @@ import (
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/cryptox"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/writer"
"mayfly-go/pkg/utils/writerx"
"mayfly-go/pkg/ws"
"strings"
"time"
@@ -196,9 +196,6 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
executedStatements++
_, err = dbConn.Exec(sql)
if err != nil {
return err
}
return err
})
@@ -258,7 +255,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
Tables: tables,
DumpDDL: needStruct,
DumpData: needData,
Writer: writer.NewGzipWriter(rc.GetWriter()),
Writer: writerx.NewGzipWriter(rc.GetWriter()),
}))
rc.ReqParam = collx.Kvs("db", dbConn.Info, "database", dbName, "tables", tablesStr, "dumpType", dumpType)

View File

@@ -1,30 +1,29 @@
package api
import (
"context"
"fmt"
"io"
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/sqlparser"
"mayfly-go/internal/db/domain/entity"
fileapp "mayfly-go/internal/file/application"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/may-fly/cast"
)
type DbTransferTask struct {
@@ -34,6 +33,7 @@ type DbTransferTask struct {
TagApp tagapp.TagTree `inject:"TagTreeApp"`
MsgApp msgapp.Msg `inject:""`
DbSqlExecApp application.DbSqlExec `inject:""`
FileApp fileapp.File `inject:""`
}
func (d *DbTransferTask) Tasks(rc *req.Ctx) {
@@ -41,6 +41,7 @@ func (d *DbTransferTask) Tasks(rc *req.Ctx) {
res, err := d.DbTransferTask.GetPageList(queryCond, page, new([]vo.DbTransferTaskListVO))
biz.ErrIsNil(err)
if res.List != nil {
list := res.List.(*[]vo.DbTransferTaskListVO)
for _, item := range *list {
item.RunningState = entity.DbTransferTaskRunStateSuccess
@@ -48,6 +49,7 @@ func (d *DbTransferTask) Tasks(rc *req.Ctx) {
item.RunningState = entity.DbTransferTaskRunStateRunning
}
}
}
rc.ResData = res
}
@@ -106,13 +108,6 @@ func (d *DbTransferTask) Files(rc *req.Ctx) {
rc.ResData = res
}
func (d *DbTransferTask) FileRename(rc *req.Ctx) {
fm := &form.DbTransferFileForm{}
tFile := req.BindJsonAndCopyTo[*entity.DbTransferFile](rc, fm, new(entity.DbTransferFile))
_ = d.DbTransferFile.UpdateById(rc.MetaCtx, tFile)
rc.ReqParam = fm
}
func (d *DbTransferTask) FileDel(rc *req.Ctx) {
fileId := rc.PathParam("fileId")
rc.ReqParam = fileId // 记录操作日志
@@ -120,50 +115,11 @@ func (d *DbTransferTask) FileDel(rc *req.Ctx) {
uIds := make([]uint64, len(ids))
for _, v := range ids {
value, err := strconv.Atoi(v)
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
uIds = append(uIds, uint64(value))
uIds = append(uIds, cast.ToUint64(v))
}
biz.ErrIsNil(d.DbTransferFile.Delete(rc.MetaCtx, uIds...))
}
func (d *DbTransferTask) FileDown(rc *req.Ctx) {
fileUuid := rc.PathParam("fileUuid")
if fileUuid == "" {
panic(errorx.NewBiz("文件id不能为空"))
}
tFile := &entity.DbTransferFile{FileUuid: fileUuid}
err := d.DbTransferFile.GetByCond(model.NewModelCond(tFile).Dest(tFile))
biz.ErrIsNilAppendErr(err, "查询文件出错 %s")
// 拼接文件地址,并把文件流输出到客户端
brc := config.GetDbBackupRestore()
filePath := filepath.Join(fmt.Sprintf("%s/%d/%s.sql", brc.TransferPath, tFile.TaskId, fileUuid))
file, err := os.Open(filePath)
biz.ErrIsNilAppendErr(err, "读取文件失败:%s")
defer file.Close()
// Get the file information to set the correct response headers
fileInfo, err := file.Stat()
biz.ErrIsNilAppendErr(err, "读取文件失败:%s")
rc.ReqParam = tFile // 记录操作日志
// 如果文件名不以 .sql 结尾,则加上 .sql
if !strings.HasSuffix(tFile.FileName, ".sql") {
tFile.FileName += ".sql"
}
rc.Header("Content-Type", "application/octet-stream")
rc.Header("Content-Disposition", "attachment; filename="+tFile.FileName)
rc.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
_, err = io.Copy(rc.GetWriter(), file)
}
func (d *DbTransferTask) FileRun(rc *req.Ctx) {
fm := req.BindJsonAndValid(rc, &form.DbTransferFileRunForm{})
@@ -183,7 +139,7 @@ func (d *DbTransferTask) FileRun(rc *req.Ctx) {
if len(errInfo) > 300 {
errInfo = errInfo[:300] + "..."
}
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s]执行失败: [%s]", tFile.FileName, targetDbConn.Info.GetLogDesc(), errInfo)).WithClientId(fm.ClientId))
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s]执行失败: [%s]", tFile.FileKey, targetDbConn.Info.GetLogDesc(), errInfo)).WithClientId(fm.ClientId))
}
}()
@@ -194,13 +150,7 @@ func (d *DbTransferTask) FileRun(rc *req.Ctx) {
}
func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFileRunForm, tFile *entity.DbTransferFile, targetDbConn *dbi.DbConn) {
filePath := d.DbTransferFile.GetFilePath(tFile)
_, err := os.Stat(filePath)
biz.ErrIsNilAppendErr(err, "sql文件不存在%s")
file, err := os.Open(filePath)
biz.ErrIsNilAppendErr(err, "sql文件读取出错%s")
filename, reader, err := d.FileApp.GetReader(context.TODO(), tFile.FileKey)
executedStatements := 0
progressId := stringx.Rand(32)
@@ -213,12 +163,12 @@ func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFile
biz.ErrIsNilAppendErr(err, "连接目标数据库失败: %s")
}
err = sqlparser.SQLSplit(file, func(sql string) error {
err = sqlparser.SQLSplit(reader, func(sql string) error {
select {
case <-ticker.C:
ws.SendJsonMsg(ws.UserId(laId), fm.ClientId, msgdto.InfoSqlProgressMsg("sql脚本执行进度", &progressMsg{
Id: progressId,
Title: tFile.FileName,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
@@ -233,5 +183,5 @@ func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFile
biz.ErrIsNilAppendErr(err, "执行sql失败: %s")
}
d.MsgApp.CreateAndSend(la, msgdto.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("sql脚本执行完成%s", tFile.FileName)).WithClientId(fm.ClientId))
d.MsgApp.CreateAndSend(la, msgdto.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("sql脚本执行完成%s", filename)).WithClientId(fm.ClientId))
}

View File

@@ -7,7 +7,6 @@ type DbTransferFileListVO struct {
CreateTime *time.Time `json:"createTime"`
Status int8 `json:"status"`
FileDbType string `json:"fileDbType"`
FileName string `json:"fileName"`
FileUuid string `json:"fileUuid"`
FileKey string `json:"fileKey"`
LogId uint64 `json:"logId"` // 日志ID
}

View File

@@ -18,6 +18,7 @@ import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/writerx"
"sort"
"strings"
"time"
@@ -231,7 +232,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
log = reqParam.Log
}
writer := reqParam.Writer
writer := writerx.NewStringWriter(reqParam.Writer)
defer writer.Close()
dbId := reqParam.DbId
dbName := reqParam.DbName
@@ -241,6 +242,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
if err != nil {
return err
}
writer.WriteString("\n-- ----------------------------")
writer.WriteString("\n-- 导出平台: mayfly-go")
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", time.Now().Format("2006-01-02 15:04:05")))
@@ -306,7 +308,6 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
log(fmt.Sprintf("获取表[%s]信息...", tableName))
quoteTableName := targetMeta.QuoteIdentifier(tableName)
writer.TryFlush()
// 查询表信息,主要是为了查询表注释
tbs, err := srcMeta.GetTables(tableName)
if err != nil {

View File

@@ -5,11 +5,11 @@ import (
"context"
"encoding/hex"
"fmt"
"github.com/google/uuid"
"mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
fileapp "mayfly-go/internal/file/application"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/base"
@@ -20,12 +20,13 @@ import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/writer"
"os"
"mayfly-go/pkg/utils/timex"
"sort"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
)
@@ -59,7 +60,8 @@ type dbTransferAppImpl struct {
dbApp Db `inject:"DbApp"`
logApp sysapp.Syslog `inject:"SyslogApp"`
fileApp DbTransferFile `inject:"DbTransferFileApp"`
transferFileApp DbTransferFile `inject:"DbTransferFileApp"`
fileApp fileapp.File `inject:"FileApp"`
}
func (app *dbTransferAppImpl) InjectDbTransferTaskRepo(repo repository.DbTransferTask) {
@@ -133,7 +135,7 @@ func (app *dbTransferAppImpl) InitCronJob() {
}
}
// 把所有运行中的文件状态设置为失败
_ = app.fileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
_ = app.transferFileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
// 把所有需要定时执行的任务添加到定时任务中
pageParam := &model.PageParam{
@@ -255,7 +257,6 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, lo
}
func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
// 1、新增迁移文件数据
nowTime := time.Now()
tFile := &entity.DbTransferFile{
@@ -263,14 +264,16 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
CreateTime: &nowTime,
Status: entity.DbTransferFileStatusRunning,
FileDbType: cmp.Or(task.TargetFileDbType, task.TargetDbType),
FileName: fmt.Sprintf("%s.sql", task.TaskName), // 用于下载和展示
FileUuid: uuid.New().String(), // 用于存放到磁盘
LogId: logId,
}
_ = app.fileApp.Save(ctx, tFile)
_ = app.transferFileApp.Save(ctx, tFile)
// 新建一个文件,文件位置为 {transferPath}/{taskId}/{uuid}.sql
filePath := app.fileApp.GetFilePath(tFile)
filename := fmt.Sprintf("dtf_%s_%s.sql", task.TaskName, timex.TimeNo())
fileKey, writer, saveFileFunc, err := app.fileApp.NewWriter(ctx, "", filename)
if err != nil {
app.EndTransfer(ctx, logId, taskId, "创建文件失败", err, nil)
return
}
// 从tables提取表名
tableNames := make([]string, 0)
@@ -278,11 +281,12 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
tableNames = append(tableNames, table.TableName)
}
// 2、把源库数据迁移到文件
app.Log(ctx, logId, fmt.Sprintf("开始迁移表数据到文件: %s", filePath))
app.Log(ctx, logId, fmt.Sprintf("开始迁移表数据到文件: %s", filename))
app.Log(ctx, logId, fmt.Sprintf("目标库文件语言类型: %s", task.TargetFileDbType))
go func() {
defer saveFileFunc()
defer app.MarkStop(taskId)
defer app.logApp.Flush(logId, true)
ctx = context.Background()
@@ -294,7 +298,7 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
Tables: tableNames,
DumpDDL: true,
DumpData: true,
Writer: writer.NewFileWriter(filePath),
Writer: writer,
Log: func(msg string) { // 记录日志
app.Log(ctx, logId, msg)
},
@@ -302,15 +306,16 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
if err != nil {
app.EndTransfer(ctx, logId, taskId, "数据库迁移失败", err, nil)
tFile.Status = entity.DbTransferFileStatusFail
_ = app.fileApp.UpdateById(ctx, tFile)
_ = app.transferFileApp.UpdateById(ctx, tFile)
// 删除文件
_ = os.Remove(filePath)
_ = app.fileApp.Remove(ctx, fileKey)
return
}
app.EndTransfer(ctx, logId, taskId, "数据库迁移完成", err, nil)
tFile.Status = entity.DbTransferFileStatusSuccess
_ = app.fileApp.UpdateById(ctx, tFile)
tFile.FileKey = fileKey
_ = app.transferFileApp.UpdateById(ctx, tFile)
}()
}

View File

@@ -2,15 +2,11 @@ package application
import (
"context"
"fmt"
"github.com/google/uuid"
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
fileapp "mayfly-go/internal/file/application"
"mayfly-go/pkg/base"
"mayfly-go/pkg/model"
"os"
"path/filepath"
)
type DbTransferFile interface {
@@ -22,14 +18,14 @@ type DbTransferFile interface {
Save(ctx context.Context, instanceEntity *entity.DbTransferFile) error
Delete(ctx context.Context, id ...uint64) error
GetFilePath(ent *entity.DbTransferFile) string
}
var _ DbTransferFile = (*dbTransferFileAppImpl)(nil)
type dbTransferFileAppImpl struct {
base.AppImpl[*entity.DbTransferFile, repository.DbTransferFile]
fileApp fileapp.File `inject:"FileApp"`
}
func (app *dbTransferFileAppImpl) InjectDbTransferFileRepo(repo repository.DbTransferFile) {
@@ -51,28 +47,16 @@ func (app *dbTransferFileAppImpl) Save(ctx context.Context, taskEntity *entity.D
}
func (app *dbTransferFileAppImpl) Delete(ctx context.Context, id ...uint64) error {
arr, err := app.GetByIds(id, "task_id", "file_uuid")
arr, err := app.GetByIds(id, "task_id", "file_key")
if err != nil {
return err
}
// 删除对应的文件
for _, file := range arr {
_ = os.Remove(app.GetFilePath(file))
_ = app.fileApp.Remove(ctx, file.FileKey)
}
// 删除数据
return app.DeleteById(ctx, id...)
}
func (app *dbTransferFileAppImpl) GetFilePath(ent *entity.DbTransferFile) string {
brc := config.GetDbBackupRestore()
if ent.FileUuid == "" {
ent.FileUuid = uuid.New().String()
}
filePath := filepath.Join(fmt.Sprintf("%s/%d/%s.sql", brc.TransferPath, ent.TaskId, ent.FileUuid))
return filePath
}

View File

@@ -1,10 +1,10 @@
package dto
import (
"io"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/utils/writer"
)
type SaveDbInstance struct {
@@ -22,7 +22,7 @@ type DumpDb struct {
LogId uint64
Writer writer.CustomWriter
Writer io.WriteCloser
Log func(msg string)
TargetDbType dbi.DbType
}

View File

@@ -71,15 +71,19 @@ func parseSQL(r io.Reader, callback SQLCallback) error {
buffer.Next(size)
case inString:
if escapeNextChar {
// 当前字符是转义后的字符,直接写入。如后一个为" 避免进入r==stringDelimiter判断被当做字符串结束符中断
currentStatement.WriteRune(r)
escapeNextChar = false
} else if r == '\\' {
// 当前字符是转义符,设置标志位并写入
escapeNextChar = true
currentStatement.WriteRune(r)
} else if r == stringDelimiter {
// 当前字符是字符串结束符,结束字符串处理
inString = false
currentStatement.WriteRune(r)
} else {
// 其他字符,直接写入
currentStatement.WriteRune(r)
}
buffer.Next(size)

View File

@@ -13,8 +13,7 @@ type DbTransferFile struct {
TaskId uint64 `orm:"column(task_id)" json:"taskId"` // 迁移任务ID
LogId uint64 `orm:"column(log_id)" json:"logId"` // 日志ID
FileDbType string `orm:"column(file_db_type)" json:"fileDbType"` // sql文件数据库类型
FileName string `orm:"column(file_name)" json:"fileName"` // 显式文件
FileUuid string `orm:"column(file_uuid)" json:"fileUuid"` // 文件真实id拼接后可以下载
FileKey string `orm:"column(file_key)" json:"fileKey"` // 文件
}
func (d *DbTransferFile) TableName() string {

View File

@@ -37,15 +37,10 @@ func InitDbTransferRouter(router *gin.RouterGroup) {
// 导出文件管理-列表
req.NewGet("/files/:taskId", d.Files),
req.NewPost("/files/rename", d.FileRename).Log(req.NewLogSave("dts-删除迁移文件")).RequiredPermissionCode("db:transfer:files:rename"),
// 导出文件管理-删除
req.NewPost("/files/del/:fileId", d.FileDel).Log(req.NewLogSave("dts-删除迁移文件")).RequiredPermissionCode("db:transfer:files:del"),
req.NewPost("/files/run", d.FileRun).Log(req.NewLogSave("dts-执行sql文件")).RequiredPermissionCode("db:transfer:files:run"),
// 导出文件管理-下载
req.NewGet("/files/down/:fileUuid", d.FileDown).Log(req.NewLogSave("dts-下载迁移文件")).RequiredPermissionCode("db:transfer:files:down"),
}
req.BatchSetGroup(instances, reqs[:])

View File

@@ -0,0 +1,53 @@
package api
import (
"mayfly-go/internal/file/api/vo"
"mayfly-go/internal/file/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"strings"
)
type File struct {
FileApp application.File `inject:""`
}
func (f *File) GetFileByKeys(rc *req.Ctx) {
keysStr := rc.PathParam("keys")
biz.NotEmpty(keysStr, "keys不能为空")
var files []vo.SimpleFile
err := f.FileApp.ListByCondToAny(model.NewCond().In("file_key", strings.Split(keysStr, ",")), &files)
biz.ErrIsNil(err)
rc.ResData = files
}
func (f *File) GetFileContent(rc *req.Ctx) {
key := rc.PathParam("key")
biz.NotEmpty(key, "key不能为空")
filename, reader, err := f.FileApp.GetReader(rc.MetaCtx, key)
if err != nil {
rc.GetWriter().Write([]byte(err.Error()))
return
}
defer reader.Close()
rc.Download(reader, filename)
}
func (f *File) Upload(rc *req.Ctx) {
multipart, err := rc.GetRequest().MultipartReader()
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
file, err := multipart.NextPart()
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
defer file.Close()
fileKey, err := f.FileApp.Upload(rc.MetaCtx, rc.Query("fileKey"), file.FileName(), file)
biz.ErrIsNil(err)
rc.ResData = fileKey
}
func (f *File) Remove(rc *req.Ctx) {
biz.ErrIsNil(f.FileApp.Remove(rc.MetaCtx, rc.PathParam("key")))
}

View File

@@ -0,0 +1,7 @@
package vo
type SimpleFile struct {
Filename string `json:"filename"`
FileKey string `json:"fileKey"`
Size int64 `json:"size"`
}

View File

@@ -0,0 +1,9 @@
package application
import (
"mayfly-go/pkg/ioc"
)
func InitIoc() {
ioc.Register(new(fileAppImpl), ioc.WithComponentName("FileApp"))
}

View File

@@ -0,0 +1,169 @@
package application
import (
"context"
"io"
"mayfly-go/internal/file/config"
"mayfly-go/internal/file/domain/entity"
"mayfly-go/internal/file/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/writerx"
"os"
"path/filepath"
"time"
"github.com/may-fly/cast"
)
type File interface {
base.App[*entity.File]
// Upload 上传文件
//
// @param fileKey 文件key若存在则使用存在的文件key否则生成新的文件key。
//
// @param filename 文件名,带文件后缀
//
// @return fileKey 文件key
Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error)
// NewWriter 创建文件writer
//
// @param canEmptyFileKey 文件key若不为空则使用该文件key否则生成新的文件key。
//
// @param filename 文件名,带文件后缀
//
// @return fileKey 文件key
//
// @return writer 文件writer
//
// @return saveFunc 保存文件信息的回调函数 (必须要defer中调用才会入库保存该文件信息)
NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error)
// GetReader 获取文件reader
//
// @return filename 文件名
//
// @return reader 文件reader
//
// @return err 错误
GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error)
// Remove 删除文件
Remove(ctx context.Context, fileKey string) error
}
type fileAppImpl struct {
base.AppImpl[*entity.File, repository.File]
}
func (f *fileAppImpl) InjectFileRepo(repo repository.File) {
f.Repo = repo
}
func (f *fileAppImpl) Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error) {
fileKey, writer, saveFileFunc, err := f.NewWriter(ctx, fileKey, filename)
if err != nil {
return fileKey, err
}
defer saveFileFunc()
if _, err := io.Copy(writer, r); err != nil {
return fileKey, err
}
return fileKey, nil
}
func (f *fileAppImpl) NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error) {
isNewFile := true
file := &entity.File{}
if canEmptyFileKey == "" {
canEmptyFileKey = stringx.RandUUID()
file.FileKey = canEmptyFileKey
} else {
file.FileKey = canEmptyFileKey
if err := f.GetByCond(file); err == nil {
isNewFile = false
}
}
file.Filename = filename
if !isNewFile {
// 先删除旧文件
f.remove(ctx, file)
}
// 生产新的文件名
newFilename := canEmptyFileKey + filepath.Ext(filename)
filepath, w, err := f.newWriter(newFilename)
if err != nil {
return "", nil, nil, err
}
file.Path = filepath
fileKey = canEmptyFileKey
writer = writerx.NewCountingWriteCloser(w)
// 创建回调函数
saveFunc = func() error {
// 获取已写入的字节数
file.Size = writer.BytesWritten()
writer.Close()
// 保存文件信息
return f.Save(ctx, file)
}
return fileKey, writer, saveFunc, nil
}
func (f *fileAppImpl) GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error) {
file := &entity.File{FileKey: fileKey}
if err := f.GetByCond(file); err != nil {
return "", nil, errorx.NewBiz("文件不存在")
}
r, err := os.Open(filepath.Join(config.GetFileConfig().BasePath, file.Path))
return file.Filename, r, err
}
func (f *fileAppImpl) Remove(ctx context.Context, fileKey string) error {
file := &entity.File{FileKey: fileKey}
if err := f.GetByCond(file); err != nil {
return errorx.NewBiz("文件不存在")
}
f.DeleteById(ctx, file.Id)
return f.remove(ctx, file)
}
func (f *fileAppImpl) newWriter(filename string) (string, io.WriteCloser, error) {
now := time.Now()
filePath := filepath.Join(cast.ToString(now.Year()), cast.ToString(int(now.Month())), cast.ToString(now.Day()), cast.ToString(now.Hour()), filename)
fileAbsPath := filepath.Join(config.GetFileConfig().BasePath, filePath)
// 目录不存在则创建
fileDir := filepath.Dir(fileAbsPath)
if _, err := os.Stat(fileDir); os.IsNotExist(err) {
err = os.MkdirAll(fileDir, os.ModePerm)
if err != nil {
return "", nil, err
}
}
// 创建目标文件
out, err := os.OpenFile(fileAbsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
if err != nil {
return "", nil, err
}
return filePath, out, nil
}
func (f *fileAppImpl) remove(ctx context.Context, file *entity.File) error {
if err := os.Remove(filepath.Join(config.GetFileConfig().BasePath, file.Path)); err != nil {
logx.ErrorfContext(ctx, "删除旧文件[%s] 失败: %s", file.Path, err.Error())
return err
}
return nil
}

View File

@@ -0,0 +1,24 @@
package config
import (
sysapp "mayfly-go/internal/sys/application"
"github.com/may-fly/cast"
)
const (
ConfigKeyFile string = "FileConfig" // 文件配置key
)
type FileConfig struct {
BasePath string // 文件基础路径
}
func GetFileConfig() *FileConfig {
c := sysapp.GetConfigApp().GetConfig(ConfigKeyFile)
jm := c.GetJsonMap()
fc := new(FileConfig)
fc.BasePath = cast.ToStringD(jm["basePath"], "./file")
return fc
}

View File

@@ -0,0 +1,16 @@
package entity
import "mayfly-go/pkg/model"
type File struct {
model.Model
FileKey string `json:"fikeKey"` // 文件key
Filename string `json:"filename"` // 文件名
Path string `json:"path" ` // 文件路径
Size int64 `json:"size"`
}
func (a *File) TableName() string {
return "t_sys_file"
}

View File

@@ -0,0 +1,5 @@
package entity
type FileQuery struct {
Keys []string
}

View File

@@ -0,0 +1,10 @@
package repository
import (
"mayfly-go/internal/file/domain/entity"
"mayfly-go/pkg/base"
)
type File interface {
base.Repo[*entity.File]
}

View File

@@ -0,0 +1,15 @@
package persistence
import (
"mayfly-go/internal/file/domain/entity"
"mayfly-go/internal/file/domain/repository"
"mayfly-go/pkg/base"
)
type fileRepoImpl struct {
base.RepoImpl[*entity.File]
}
func newFileRepo() repository.File {
return &fileRepoImpl{base.RepoImpl[*entity.File]{M: new(entity.File)}}
}

View File

@@ -0,0 +1,9 @@
package persistence
import (
"mayfly-go/pkg/ioc"
)
func InitIoc() {
ioc.Register(newFileRepo(), ioc.WithComponentName("FileRepo"))
}

View File

@@ -0,0 +1,16 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/file/application"
"mayfly-go/internal/file/infrastructure/persistence"
"mayfly-go/internal/file/router"
)
func init() {
initialize.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
})
initialize.AddInitRouterFunc(router.Init)
}

View File

@@ -0,0 +1,29 @@
package router
import (
"mayfly-go/internal/file/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitFileRouter(router *gin.RouterGroup) {
file := router.Group("sys/files")
f := new(api.File)
biz.ErrIsNil(ioc.Inject(f))
reqs := [...]*req.Conf{
req.NewGet("/detail/:keys", f.GetFileByKeys).DontNeedToken(),
req.NewGet("/:key", f.GetFileContent).DontNeedToken().NoRes(),
req.NewPost("/upload", f.Upload).Log(req.NewLogSave("file-文件上传")),
req.NewDelete("/:key", f.Remove).Log(req.NewLogSave("file-文件删除")),
}
req.BatchSetGroup(file, reqs[:])
}

View File

@@ -0,0 +1,9 @@
package router
import (
"github.com/gin-gonic/gin"
)
func Init(router *gin.RouterGroup) {
InitFileRouter(router)
}

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/base64"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/event"
@@ -9,7 +8,6 @@ import (
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/application/dto"
"mayfly-go/internal/machine/config"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/guac"
"mayfly-go/internal/machine/mcm"
@@ -25,8 +23,6 @@ import (
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/ws"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
@@ -243,15 +239,6 @@ func (m *Machine) MachineTermOpRecords(rc *req.Ctx) {
rc.ResData = res
}
func (m *Machine) MachineTermOpRecord(rc *req.Ctx) {
termOp, err := m.MachineTermOpApp.GetById(uint64(rc.PathParamInt("recId")))
biz.ErrIsNil(err)
bytes, err := os.ReadFile(path.Join(config.GetMachine().TerminalRecPath, termOp.RecordFilePath))
biz.ErrIsNilAppendErr(err, "读取终端操作记录失败: %s")
rc.ResData = base64.StdEncoding.EncodeToString(bytes)
}
const (
SocketTimeout = 15 * time.Second
MaxGuacMessage = 8192

View File

@@ -3,6 +3,7 @@ package application
import (
"context"
"fmt"
fileapp "mayfly-go/internal/file/application"
"mayfly-go/internal/machine/config"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
@@ -15,8 +16,7 @@ import (
"mayfly-go/pkg/scheduler"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/stringx"
"os"
"path"
"mayfly-go/pkg/utils/timex"
"time"
"github.com/gorilla/websocket"
@@ -38,6 +38,7 @@ type machineTermOpAppImpl struct {
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]
machineCmdConfApp MachineCmdConf `inject:"MachineCmdConfApp"`
fileApp fileapp.File `inject:"FileApp"`
}
// 注入MachineTermOpRepo
@@ -63,20 +64,14 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
termOpRecord.MachineId = cli.Info.Id
termOpRecord.Username = cli.Info.Username
// 回放文件路径为: 基础配置路径/机器编号/操作日期(202301)/day/hour/randstr.cast
recRelPath := path.Join(cli.Info.Code, now.Format("200601"), fmt.Sprintf("%d", now.Day()), fmt.Sprintf("%d", now.Hour()))
// 文件绝对路径
recAbsPath := path.Join(config.GetMachine().TerminalRecPath, recRelPath)
os.MkdirAll(recAbsPath, 0766)
filename := fmt.Sprintf("%s.cast", stringx.RandByChars(18, stringx.LowerChars))
f, err := os.OpenFile(path.Join(recAbsPath, filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
fileKey, wc, saveFileFunc, err := m.fileApp.NewWriter(ctx, "", fmt.Sprintf("mto_%d_%s.cast", termOpRecord.MachineId, timex.TimeNo()))
if err != nil {
return errorx.NewBiz("创建终端回放记录文件失败: %s", err.Error())
}
defer f.Close()
defer saveFileFunc()
termOpRecord.RecordFilePath = path.Join(recRelPath, filename)
recorder = mcm.NewRecorder(f)
termOpRecord.FileKey = fileKey
recorder = mcm.NewRecorder(wc)
}
createTsParam := &mcm.CreateTerminalSessionParam{
@@ -134,9 +129,8 @@ func (m *machineTermOpAppImpl) TimerDeleteTermOp() {
return
}
basePath := config.GetMachine().TerminalRecPath
for _, termOp := range termOps {
if err := m.DeleteTermOp(basePath, termOp); err != nil {
if err := m.DeleteTermOp(termOp); err != nil {
logx.Warnf("删除终端操作记录失败: %s", err.Error())
}
}
@@ -144,10 +138,10 @@ func (m *machineTermOpAppImpl) TimerDeleteTermOp() {
}
// 删除终端记录即对应文件
func (m *machineTermOpAppImpl) DeleteTermOp(basePath string, termOp *entity.MachineTermOp) error {
func (m *machineTermOpAppImpl) DeleteTermOp(termOp *entity.MachineTermOp) error {
if err := m.DeleteById(context.Background(), termOp.Id); err != nil {
return err
}
return os.Remove(path.Join(basePath, termOp.RecordFilePath))
return m.fileApp.Remove(context.TODO(), termOp.FileKey)
}

View File

@@ -13,13 +13,11 @@ const (
)
type Machine struct {
TerminalRecPath string // 终端操作记录存储位置
UploadMaxFileSize int64 // 允许上传的最大文件size
TermOpSaveDays int // 终端记录保存天数
GuacdHost string // guacd服务地址 默认 127.0.0.1
GuacdPort int // guacd服务端口 默认 4822
GuacdFilePath string // guacd服务文件存储位置用于挂载RDP文件夹
GuacdRecPath string // guacd服务记录存储位置用于记录rdp操作记录
}
// 获取机器相关配置
@@ -29,12 +27,6 @@ func GetMachine() *Machine {
mc := new(Machine)
terminalRecPath := jm["terminalRecPath"]
if terminalRecPath == "" {
terminalRecPath = "./rec"
}
mc.TerminalRecPath = terminalRecPath
// 将1GB等字符串转为int64的byte
uploadMaxFileSizeStr := jm["uploadMaxFileSize"]
var uploadMaxFileSize int64 = 1 * bytex.GB
@@ -51,7 +43,6 @@ func GetMachine() *Machine {
mc.GuacdHost = cast.ToString(jm["guacdHost"])
mc.GuacdPort = cast.ToIntD(jm["guacdPort"], 4822)
mc.GuacdFilePath = cast.ToStringD(jm["guacdFilePath"], "")
mc.GuacdRecPath = cast.ToStringD(jm["guacdRecPath"], "")
return mc
}

View File

@@ -10,7 +10,7 @@ type MachineTermOp struct {
MachineId uint64 `json:"machineId"`
Username string `json:"username"`
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
FileKey string `json:"fileKey"` // 文件key
ExecCmds string `json:"execCmds"` // 执行的命令
CreateTime *time.Time `json:"createTime"`

View File

@@ -47,9 +47,6 @@ func InitMachineRouter(router *gin.RouterGroup) {
// 获取机器终端回放记录列表,目前具有保存机器信息的权限标识才有权限查看终端回放
req.NewGet(":machineId/term-recs", m.MachineTermOpRecords).RequiredPermission(saveMachineP),
// 获取机器终端回放记录
req.NewGet(":machineId/term-recs/:recId", m.MachineTermOpRecord).RequiredPermission(saveMachineP),
}
req.BatchSetGroup(machines, reqs[:])

View File

@@ -4,6 +4,7 @@ import (
_ "mayfly-go/internal/auth/init"
_ "mayfly-go/internal/common/init"
_ "mayfly-go/internal/db/init"
_ "mayfly-go/internal/file/init"
_ "mayfly-go/internal/flow/init"
_ "mayfly-go/internal/machine/init"
_ "mayfly-go/internal/mongo/init"

View File

@@ -9,10 +9,16 @@ import (
const DefaultDateTimeFormat = "2006-01-02 15:04:05"
// DefaultFormat 使用默认格式进行格式化: 2006-01-02 15:04:05
func DefaultFormat(time time.Time) string {
return time.Format(DefaultDateTimeFormat)
}
// TimeNo 获取当前时间编号格式为20060102150405
func TimeNo() string {
return time.Now().Format("20060102150405")
}
func NewNullTime(t time.Time) NullTime {
return NullTime{
NullTime: sql.NullTime{

View File

@@ -1,51 +0,0 @@
package writer
import (
"io"
"os"
"path/filepath"
)
type FileWriter struct {
tryFlushCount int
writer *os.File
aborted bool
}
func NewFileWriter(filePath string) *FileWriter {
if filePath == "" {
panic("filePath is empty")
}
// 使用filepath.Dir函数提取文件夹路径
dir := filepath.Dir(filePath)
if dir != "" {
// 检查文件夹路径,不存在则创建
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
}
}
fw, err := os.Create(filePath)
if err != nil {
panic(err)
}
return &FileWriter{writer: fw}
}
func (f *FileWriter) Close() {
f.writer.Close()
}
func (f *FileWriter) TryFlush() {
}
func (f *FileWriter) Write(b []byte) (n int, err error) {
return f.writer.Write(b)
}
func (f *FileWriter) WriteString(data string) {
io.WriteString(f.writer, data)
}

View File

@@ -1,10 +0,0 @@
package writer
import "io"
type CustomWriter interface {
io.Writer
WriteString(data string)
Close()
TryFlush()
}

View File

@@ -0,0 +1,26 @@
package writerx
import "io"
type CountingWriteCloser struct {
w io.WriteCloser
n int64 // 已写入的字节数
}
func (c *CountingWriteCloser) Write(p []byte) (int, error) {
n, err := c.w.Write(p)
c.n += int64(n)
return n, err
}
func (c *CountingWriteCloser) Close() error {
return c.w.Close()
}
func (c *CountingWriteCloser) BytesWritten() int64 {
return c.n
}
func NewCountingWriteCloser(writer io.WriteCloser) *CountingWriteCloser {
return &CountingWriteCloser{w: writer}
}

View File

@@ -1,4 +1,4 @@
package writer
package writerx
import (
"compress/gzip"
@@ -38,8 +38,8 @@ func (g *GzipWriter) Write(p []byte) (n int, err error) {
return
}
func (g *GzipWriter) Close() {
g.writer.Close()
func (g *GzipWriter) Close() error {
return g.writer.Close()
}
func (g *GzipWriter) TryFlush() {

View File

@@ -0,0 +1,21 @@
package writerx
import (
"io"
)
type StringWriter struct {
io.WriteCloser
}
func (sw *StringWriter) WriteString(s string) (n int, err error) {
return sw.WriteCloser.Write([]byte(s))
}
func (sw *StringWriter) Close() error {
return sw.WriteCloser.Close()
}
func NewStringWriter(writer io.WriteCloser) *StringWriter {
return &StringWriter{WriteCloser: writer}
}

View File

@@ -101,8 +101,7 @@ CREATE TABLE `t_db_transfer_files` (
`task_id` bigint COMMENT '迁移任务ID',
`log_id` bigint COMMENT '日志ID',
`file_db_type` varchar(200) COMMENT 'sql文件数据库类型',
`file_name` varchar(200) COMMENT '显式文件名 默认: 年月日时分秒.zip',
`file_uuid` varchar(50) COMMENT '文件真实uuid拼接后可以下载',
`file_key` varchar(50) COMMENT '文件',
PRIMARY KEY (id)
) COMMENT '数据库迁移文件管理';
@@ -668,11 +667,11 @@ INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, cre
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('oauth2登录配置', 'Oauth2Login', '[{"name":"是否启用","model":"enable","placeholder":"是否启用oauth2登录","options":"true,false"},{"name":"名称","model":"name","placeholder":"oauth2名称"},{"name":"Client ID","model":"clientId","placeholder":"Client ID"},{"name":"Client Secret","model":"clientSecret","placeholder":"Client Secret"},{"name":"Authorization URL","model":"authorizationURL","placeholder":"Authorization URL"},{"name":"AccessToken URL","model":"accessTokenURL","placeholder":"AccessToken URL"},{"name":"Redirect URL","model":"redirectURL","placeholder":"本系统地址"},{"name":"Scopes","model":"scopes","placeholder":"Scopes"},{"name":"Resource URL","model":"resourceURL","placeholder":"获取用户信息资源地址"},{"name":"UserIdentifier","model":"userIdentifier","placeholder":"用户唯一标识字段;格式为type:fieldPath(string:username)"},{"name":"是否自动注册","model":"autoRegister","placeholder":"","options":"true,false"}]', '', 'oauth2登录相关配置信息', 'admin,', '2023-07-22 13:58:51', 1, 'admin', '2023-07-22 19:34:37', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('ldap登录配置', 'LdapLogin', '[{"name":"是否启用","model":"enable","placeholder":"是否启用","options":"true,false"},{"name":"host","model":"host","placeholder":"host"},{"name":"port","model":"port","placeholder":"port"},{"name":"bindDN","model":"bindDN","placeholder":"LDAP 服务的管理员账号,如: \\"cn=admin,dc=example,dc=com\\""},{"name":"bindPwd","model":"bindPwd","placeholder":"LDAP 服务的管理员密码"},{"name":"baseDN","model":"baseDN","placeholder":"用户所在的 base DN, 如: \\"ou=users,dc=example,dc=com\\""},{"name":"userFilter","model":"userFilter","placeholder":"过滤用户的方式, 如: \\"(uid=%s)、(&(objectClass=organizationalPerson)(uid=%s))\\""},{"name":"uidMap","model":"uidMap","placeholder":"用户id和 LDAP 字段名之间的映射关系,如: cn"},{"name":"udnMap","model":"udnMap","placeholder":"用户姓名(dispalyName)和 LDAP 字段名之间的映射关系,如: displayName"},{"name":"emailMap","model":"emailMap","placeholder":"用户email和 LDAP 字段名之间的映射关系"},{"name":"skipTLSVerify","model":"skipTLSVerify","placeholder":"客户端是否跳过 TLS 证书验证","options":"true,false"},{"name":"安全协议","model":"securityProtocol","placeholder":"安全协议为Null不使用安全协议如: StartTLS, LDAPS","options":"Null,StartTLS,LDAPS"}]', '', 'ldap登录相关配置', 'admin,', '2023-08-25 21:47:20', 1, 'admin', '2023-08-25 22:56:07', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('系统全局样式设置', 'SysStyleConfig', '[{"model":"logoIcon","name":"logo图标","placeholder":"系统logo图标base64编码, 建议svg格式不超过10k","required":false},{"model":"title","name":"菜单栏标题","placeholder":"系统菜单栏标题展示","required":false},{"model":"viceTitle","name":"登录页标题","placeholder":"登录页标题展示","required":false},{"model":"useWatermark","name":"是否启用水印","placeholder":"是否启用系统水印","options":"true,false","required":false},{"model":"watermarkContent","name":"水印补充信息","placeholder":"额外水印信息","required":false}]', '{"title":"mayfly-go","viceTitle":"mayfly-go","logoIcon":"","useWatermark":"true","watermarkContent":""}', '系统icon、标题、水印信息等配置', 'all', '2024-01-04 15:17:18', 1, 'admin', '2024-01-05 09:40:44', 1, 'admin', 0, NULL);
INSERT INTO t_sys_config ( name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"},{"name":"guacd服务记录存储位置","model":"guacdRecPath","placeholder":"guacd服务记录存储位置用于记录rdp操作记录"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1000MB","termOpSaveDays":"30","guacdHost":"","guacdPort":"","guacdFilePath":"./guacd/rdp-file","guacdRecPath":"./guacd/rdp-rec"}', '机器相关配置,如终端回放路径等', 'all', '2023-07-13 16:26:44', 1, 'admin', '2024-04-06 12:25:03', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`id`, `name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES(10, '数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"},{"model":"transferPath","name":"迁移路径","placeholder":"数据库迁移文件存储路径"}]', '{"backupPath":"./db/backup","transferPath":"./db/transfer"}', '数据库备份恢复', 'all', '2023-12-29 09:55:26', 1, 'admin', '2024-08-27 15:22:22', 12, 'liuzongyang', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('机器相关配置', 'MachineConfig', '[{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"}]', '{"uploadMaxFileSize":"1000MB","termOpSaveDays":"30","guacdHost":"","guacdPort":"","guacdFilePath":"./guacd/rdp-file"}', '机器相关配置,如终端回放路径等', 'all', '2023-07-13 16:26:44', 1, 'admin', '2024-10-21 17:02:55', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('Mysql可执行文件', 'MysqlBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mysql/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('MariaDB可执行文件', 'MariadbBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mariadb/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('DBMS配置', 'DbmsConfig', '[{"model":"querySqlSave","name":"记录查询sql","placeholder":"是否记录查询类sql","options":"true,false"},{"model":"maxResultSet","name":"最大结果集","placeholder":"允许sql查询的最大结果集数。注: 0=不限制","options":""},{"model":"sqlExecTl","name":"sql执行时间限制","placeholder":"超过该时间(单位:秒),执行将被取消"}]', '{"querySqlSave":"false","maxResultSet":"0","sqlExecTl":"60"}', 'DBMS相关配置', 'admin,', '2024-03-06 13:30:51', 1, 'admin', '2024-03-06 14:07:16', 1, 'admin', 0, NULL);
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('文件配置', 'FileConfig', '[{"model":"basePath","name":"基础路径","placeholder":"默认为可执行文件对应目录下./file"}]', '{"basePath":"./file"}', '系统文件相关配置', 'admin,', '2024-10-20 22:30:01', 1, 'admin', '2024-10-21 13:51:17', 1, 'admin', 0, NULL);
COMMIT;
-- ----------------------------
@@ -856,7 +855,6 @@ INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `we
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724376022, 1709194669, 2, 1, '文件-删除', 'db:transfer:files:del', 1724376022, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 09:20:23', '2024-08-23 14:50:21', 'SmLcpu6c/HIURtJJA/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724395850, 1709194669, 2, 1, '文件-下载', 'db:transfer:files:down', 1724395850, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 14:50:51', '2024-08-23 14:50:51', 'SmLcpu6c/FmqK4azt/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724398262, 1709194669, 2, 1, '文件', 'db:transfer:files', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 15:31:02', '2024-08-23 15:31:16', 'SmLcpu6c/btVtrbhk/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724817775, 1709194669, 2, 1, '文件-重命名', 'db:transfer:files:rename', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-28 12:02:56', '2024-08-28 12:03:01', 'SmLcpu6c/zu4fvnuA/', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714032002, 1713875842, '12sSjal1/UnWIUhW0/0tJwC3Gf/', 2, 1, '命令配置-删除', 'cmdconf:del', 1714032002, 'null', 1, 'admin', 1, 'admin', '2024-04-25 16:00:02', '2024-04-25 16:00:02', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714031981, 1713875842, '12sSjal1/UnWIUhW0/tEzIKecl/', 2, 1, '命令配置-保存', 'cmdconf:save', 1714031981, 'null', 1, 'admin', 1, 'admin', '2024-04-25 15:59:41', '2024-04-25 15:59:41', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1713875842, 2, '12sSjal1/UnWIUhW0/', 1, 1, '安全配置', 'security', 1713875842, '{"component":"ops/machine/security/SecurityConfList","icon":"Setting","isKeepAlive":true,"routeName":"SecurityConfList"}', 1, 'admin', 1, 'admin', '2024-04-23 20:37:22', '2024-04-23 20:37:22', 0, NULL);
@@ -915,6 +913,25 @@ INSERT INTO `t_sys_role_resource` (role_id,resource_id,creator_id,creator,create
(7,1,1,'admin','2021-07-06 15:07:09', 0, NULL);
COMMIT;
DROP TABLE IF EXISTS `t_sys_file`;
CREATE TABLE `t_sys_file` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`file_key` varchar(32) NOT NULL COMMENT 'key',
`filename` varchar(255) NOT NULL COMMENT '文件名',
`path` varchar(555) NOT NULL COMMENT '文件路径',
`size` int NULL DEFAULT NULL COMMENT '文件大小',
`creator_id` bigint NULL DEFAULT NULL,
`creator` varchar(32) NULL DEFAULT NULL,
`modifier_id` bigint NULL DEFAULT NULL,
`modifier` varchar(255) NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`is_deleted` tinyint NOT NULL DEFAULT 0,
`delete_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_file_key` (`file_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统文件表';
-- ----------------------------
-- Table structure for t_tag_tree
-- ----------------------------

View File

@@ -7,12 +7,12 @@ ALTER TABLE `t_db_data_sync_task`
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724376022, 1709194669, 2, 1, '文件-删除', 'db:transfer:files:del', 1724376022, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 09:20:23', '2024-08-23 14:50:21', 'SmLcpu6c/HIURtJJA/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724395850, 1709194669, 2, 1, '文件-下载', 'db:transfer:files:down', 1724395850, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 14:50:51', '2024-08-23 14:50:51', 'SmLcpu6c/FmqK4azt/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724398262, 1709194669, 2, 1, '文件', 'db:transfer:files', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 15:31:02', '2024-08-23 15:31:16', 'SmLcpu6c/btVtrbhk/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724817775, 1709194669, 2, 1, '文件-重命名', 'db:transfer:files:rename', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-28 12:02:56', '2024-08-28 12:03:01', 'SmLcpu6c/zu4fvnuA/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724998419, 1709194669, 2, 1, '文件-执行', 'db:transfer:files:run', 1724998419, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-30 14:13:39', '2024-08-30 14:13:39', 'SmLcpu6c/qINungml/', 0, NULL);
-- 新增数据库迁移相关的系统配置
DELETE FROM `t_sys_config` WHERE `key` = 'DbBackupRestore';
INSERT INTO `t_sys_config` (`id`, `name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES(10, '数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"},{"model":"transferPath","name":"迁移路径","placeholder":"数据库迁移文件存储路径"}]', '{"backupPath":"./db/backup","transferPath":"./db/transfer"}', '数据库备份恢复', 'all', '2023-12-29 09:55:26', 1, 'admin', '2024-08-27 15:22:22', 12, 'liuzongyang', 0, NULL);
UPDATE `t_sys_config` SET param = '[{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"}]' WHERE `key`='MachineConfig';
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('文件配置', 'FileConfig', '[{"model":"basePath","name":"基础路径","placeholder":"默认为可执行文件对应目录下./file"}]', '{"basePath":"./file"}', '系统文件相关配置', 'admin,', '2024-10-20 22:30:01', 1, 'admin', '2024-10-21 13:51:17', 1, 'admin', 0, NULL);
-- 数据库迁移到文件
ALTER TABLE `t_db_transfer_task`
@@ -37,11 +37,28 @@ CREATE TABLE `t_db_transfer_files` (
`task_id` bigint COMMENT '迁移任务ID',
`log_id` bigint COMMENT '日志ID',
`file_db_type` varchar(200) COMMENT 'sql文件数据库类型',
`file_name` varchar(200) COMMENT '显式文件名 默认: 年月日时分秒.zip',
`file_uuid` varchar(50) COMMENT '文件真实uuid拼接后可以下载',
`file_key` varchar(50) COMMENT '文件',
PRIMARY KEY (id)
) COMMENT '数据库迁移文件管理';
ALTER TABLE `t_flow_procdef`
ADD COLUMN `condition` text NULL comment '触发审批的条件计算结果返回1则需要启用该流程';
CREATE TABLE `t_sys_file` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`file_key` varchar(32) NOT NULL COMMENT 'key',
`filename` varchar(255) NOT NULL COMMENT '文件名',
`path` varchar(255) NOT NULL COMMENT '文件路径',
`size` int NULL DEFAULT NULL COMMENT '文件大小',
`creator_id` bigint NULL DEFAULT NULL,
`creator` varchar(32) NULL DEFAULT NULL,
`modifier_id` bigint NULL DEFAULT NULL,
`modifier` varchar(255) NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`is_deleted` tinyint NOT NULL DEFAULT 0,
`delete_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_file_key` (`file_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统文件表';