diff --git a/src/framework/ssh/files.go b/src/framework/ssh/files.go new file mode 100644 index 00000000..8081877a --- /dev/null +++ b/src/framework/ssh/files.go @@ -0,0 +1,103 @@ +package ssh + +import ( + "fmt" + "strings" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/cmd" + "be.ems/src/framework/utils/parse" +) + +// FileListRow 文件列表行数据 +type FileListRow struct { + FileType string `json:"fileType"` // 文件类型 dir, file, symlink + FileMode string `json:"fileMode"` // 文件的权限 + LinkCount int64 `json:"linkCount"` // 硬链接数目 + Owner string `json:"owner"` // 所属用户 + Group string `json:"group"` // 所属组 + Size int64 `json:"size"` // 文件的大小 + ModifiedTime int64 `json:"modifiedTime"` // 最后修改时间,单位为秒 + FileName string `json:"fileName"` // 文件的名称 +} + +// 文件列表 +// search 文件名后模糊* +// +// return 行记录,异常 +func FileList(sshClient *ConnSSH, path, search string) ([]FileListRow, error) { + var rows []FileListRow + rowStr := "" + + // 发送命令 + searchStr := "*" + if search != "" { + searchStr = search + searchStr + } + // cd /var/log && find. -maxdepth 1 -name'mme*' -exec ls -ltd --time-style=+%s {} + + cmdStr := fmt.Sprintf("cd %s && find . -maxdepth 1 -name '%s' -exec ls -ltd --time-style=+%%s {} +", path, searchStr) + // cd /var/log && ls -ltd --time-style=+%s mme* + // cmdStr := fmt.Sprintf("cd %s && ls -ltd --time-style=+%%s %s", path, searchStr) + + // 是否远程客户端读取 + if sshClient == nil { + resultStr, err := cmd.Exec(cmdStr) + if err != nil { + logger.Errorf("Ne FileList Path: %s, Search: %s, Error:%s", path, search, err.Error()) + return rows, err + } + rowStr = resultStr + } else { + resultStr, err := sshClient.RunCMD(cmdStr) + if err != nil { + logger.Errorf("Ne FileList Path: %s, Search: %s, Error:%s", path, search, err.Error()) + return rows, err + } + rowStr = resultStr + } + + // 遍历组装 + rowStrList := strings.Split(rowStr, "\n") + for _, rowStr := range rowStrList { + if rowStr == "" { + continue + } + // 使用空格对字符串进行切割 + fields := strings.Fields(rowStr) + + // 拆分不足7位跳过 + if len(fields) != 7 { + continue + } + + // 文件类型 + fileMode := fields[0] + fileType := "file" + if fileMode[0] == 'd' { + fileType = "dir" + } else if fileMode[0] == 'l' { + fileType = "symlink" + } + + // 文件名 + fileName := fields[6] + if fileName == "." { + continue + } else if strings.HasPrefix(fileName, "./") { + fileName = strings.TrimPrefix(fileName, "./") + } + + // 提取各个字段的值 + rows = append(rows, FileListRow{ + FileMode: fileMode, + FileType: fileType, + LinkCount: parse.Number(fields[1]), + Owner: fields[2], + Group: fields[3], + Size: parse.Number(fields[4]), + ModifiedTime: parse.Number(fields[5]), + FileName: fileName, + }) + } + return rows, nil +} diff --git a/src/framework/ssh/sftp.go b/src/framework/ssh/sftp.go new file mode 100644 index 00000000..e2a0d3cf --- /dev/null +++ b/src/framework/ssh/sftp.go @@ -0,0 +1,157 @@ +package ssh + +import ( + "io" + "os" + "path/filepath" + + "be.ems/src/framework/logger" + gosftp "github.com/pkg/sftp" +) + +// SSHClientSFTP SSH客户端SFTP对象 +type SSHClientSFTP struct { + Client *gosftp.Client +} + +// Close 关闭会话 +func (s *SSHClientSFTP) Close() { + if s.Client != nil { + s.Client.Close() + } +} + +// CopyDirRemoteToLocal 复制目录-远程到本地 +func (s *SSHClientSFTP) CopyDirRemoteToLocal(remoteDir, localDir string) error { + // 创建本地目录 + err := os.MkdirAll(localDir, 0775) + if err != nil { + logger.Errorf("CopyDirRemoteToLocal failed to creating local directory %s: => %s", localDir, err.Error()) + return err + } + + // 列出远程目录中的文件和子目录 + remoteFiles, err := s.Client.ReadDir(remoteDir) + if err != nil { + logger.Errorf("CopyDirRemoteToLocal failed to reading remote directory %s: => %s", remoteDir, err.Error()) + return err + } + + // 遍历远程文件和子目录并复制到本地 + for _, remoteFile := range remoteFiles { + remotePath := filepath.ToSlash(filepath.Join(remoteDir, remoteFile.Name())) + localPath := filepath.ToSlash(filepath.Join(localDir, remoteFile.Name())) + + if remoteFile.IsDir() { + // 如果是子目录,则递归复制子目录 + err = s.CopyDirRemoteToLocal(remotePath, localPath) + if err != nil { + logger.Errorf("CopyDirRemoteToLocal failed to copying remote directory %s: => %s", remotePath, err.Error()) + continue + } + } else { + // 如果是文件,则复制文件内容 + if err := s.CopyFileRemoteToLocal(remotePath, localPath); err != nil { + logger.Errorf("CopyDirRemoteToLocal failed to opening remote file %s: => %s", remotePath, err.Error()) + continue + } + } + } + return nil +} + +// CopyDirLocalToRemote 复制目录-本地到远程 +func (s *SSHClientSFTP) CopyDirLocalToRemote(localDir, remoteDir string) error { + // 遍历本地目录中的文件和子目录并复制到远程 + err := filepath.Walk(localDir, func(localPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 生成远程路径 + remotePath := filepath.ToSlash(filepath.Join(remoteDir, localPath[len(localDir):])) + + if info.IsDir() { + // 如果是子目录,则创建远程目录 + if err := s.Client.MkdirAll(remotePath); err != nil { + logger.Errorf("CopyDirLocalToRemote failed to creating remote directory %s: => %s", remotePath, err.Error()) + return err + } + } else { + // 如果是文件,则复制文件内容 + if err := s.CopyFileLocalToRemote(localPath, remotePath); err != nil { + logger.Errorf("CopyDirLocalToRemote failed to copying remote file %s: => %s", localPath, err.Error()) + return err + } + } + + return nil + }) + if err != nil { + logger.Errorf("CopyDirLocalToRemote failed to walking remote directory: => %s", err.Error()) + return err + } + return nil +} + +// CopyFileRemoteToLocal 复制文件-远程到本地 +func (s *SSHClientSFTP) CopyFileRemoteToLocal(remotePath, localPath string) error { + if err := os.MkdirAll(filepath.Dir(localPath), 0775); err != nil { + return err + } + + // 打开远程文件 + remoteFile, err := s.Client.Open(remotePath) + if err != nil { + logger.Errorf("CopyFileRemoteToLocal failed to opening remote file: => %s", err.Error()) + return err + } + defer remoteFile.Close() + + // 创建本地文件 + localFile, err := os.Create(localPath) + if err != nil { + logger.Errorf("CopyFileRemoteToLocal failed to creating local file: => %s", err.Error()) + return err + } + defer localFile.Close() + + // 复制文件内容 + if _, err = io.Copy(localFile, remoteFile); err != nil { + logger.Errorf("CopyFileRemoteToLocal failed to copying contents: => %s", err.Error()) + return err + } + return nil +} + +// CopyFileLocalToRemote 复制文件-本地到远程 +func (s *SSHClientSFTP) CopyFileLocalToRemote(localPath, remotePath string) error { + // 打开本地文件 + localFile, err := os.Open(localPath) + if err != nil { + logger.Errorf("CopyFileLocalToRemote failed to opening local file: => %s", err.Error()) + return err + } + defer localFile.Close() + + // 创建远程目录 + if err := s.Client.MkdirAll(filepath.Dir(remotePath)); err != nil { + logger.Errorf("CopyFileLocalToRemote failed to creating remote directory %s: => %s", remotePath, err.Error()) + return err + } + + // 创建远程文件 + remoteFile, err := s.Client.Create(remotePath) + if err != nil { + logger.Errorf("CopyFileLocalToRemote failed to creating remote file: => %s", err.Error()) + return err + } + defer remoteFile.Close() + + // 复制文件内容 + if _, err = io.Copy(remoteFile, localFile); err != nil { + logger.Errorf("CopyFileLocalToRemote failed to copying contents: => %s", err.Error()) + return err + } + return nil +} diff --git a/src/framework/ssh/ssh.go b/src/framework/ssh/ssh.go new file mode 100644 index 00000000..f412b718 --- /dev/null +++ b/src/framework/ssh/ssh.go @@ -0,0 +1,229 @@ +package ssh + +import ( + "fmt" + "os" + "os/user" + "strings" + "time" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/cmd" + gosftp "github.com/pkg/sftp" + gossh "golang.org/x/crypto/ssh" +) + +// ConnSSH 连接SSH对象 +type ConnSSH struct { + User string `json:"user"` // 主机用户名 + Addr string `json:"addr"` // 主机地址 + Port int64 `json:"port"` // SSH端口 + AuthMode string `json:"authMode"` // 认证模式(0密码 1主机私钥) + Password string `json:"password"` // 认证密码 + PrivateKey string `json:"privateKey"` // 认证私钥 + PassPhrase string `json:"passPhrase"` // 认证私钥密码 + + DialTimeOut time.Duration `json:"dialTimeOut"` // 连接超时断开 + + Client *gossh.Client `json:"client"` + LastResult string `json:"lastResult"` // 记最后一次执行命令的结果 +} + +// NewClient 创建SSH客户端 +func (c *ConnSSH) NewClient() (*ConnSSH, error) { + // IPV6地址协议 + proto := "tcp" + if strings.Contains(c.Addr, ":") { + proto = "tcp6" + c.Addr = fmt.Sprintf("[%s]", c.Addr) + } + addr := fmt.Sprintf("%s:%d", c.Addr, c.Port) + + // ssh客户端配置 + config := &gossh.ClientConfig{} + config.SetDefaults() + config.HostKeyCallback = gossh.InsecureIgnoreHostKey() + config.User = c.User + + // 默认等待5s + if c.DialTimeOut == 0 { + c.DialTimeOut = 5 * time.Second + } + config.Timeout = c.DialTimeOut + + // 认证模式-0密码 1私钥 + if c.AuthMode == "1" { + var signer gossh.Signer + var err error + if len(c.PassPhrase) != 0 { + signer, err = gossh.ParsePrivateKeyWithPassphrase([]byte(c.PrivateKey), []byte(c.PassPhrase)) + } else { + signer, err = gossh.ParsePrivateKey([]byte(c.PrivateKey)) + } + if err != nil { + logger.Errorf("NewClient parse private key => %s", err.Error()) + return nil, err + } + config.Auth = []gossh.AuthMethod{gossh.PublicKeys(signer)} + } else { + config.Auth = []gossh.AuthMethod{gossh.Password(c.Password)} + } + + client, err := gossh.Dial(proto, addr, config) + if nil != err { + logger.Errorf("NewClient dial => %s", err.Error()) + return c, err + } + c.Client = client + return c, nil +} + +// Close 关闭当前SSH客户端 +func (c *ConnSSH) Close() { + if c.Client != nil { + c.Client.Close() + } +} + +// RunCMD 执行单次命令 +func (c *ConnSSH) RunCMD(cmd string) (string, error) { + if c.Client == nil { + if _, err := c.NewClient(); err != nil { + return "", err + } + } + session, err := c.Client.NewSession() + if err != nil { + logger.Errorf("RunCMD failed to create session: => %s", err.Error()) + return "", err + } + defer session.Close() + buf, err := session.CombinedOutput(cmd) + if err != nil { + logger.Infof("RunCMD failed run command: => %s", cmd) + logger.Errorf("RunCMD failed run command: => %s", err.Error()) + } + c.LastResult = string(buf) + return c.LastResult, err +} + +// NewClientSession 创建SSH客户端会话对象 +func (c *ConnSSH) NewClientSession(cols, rows int) (*SSHClientSession, error) { + sshSession, err := c.Client.NewSession() + if err != nil { + return nil, err + } + + stdin, err := sshSession.StdinPipe() + if err != nil { + return nil, err + } + + comboWriter := new(singleWriter) + sshSession.Stdout = comboWriter + sshSession.Stderr = comboWriter + + modes := gossh.TerminalModes{ + gossh.ECHO: 1, + gossh.TTY_OP_ISPEED: 14400, + gossh.TTY_OP_OSPEED: 14400, + } + + if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { + return nil, err + } + + if err := sshSession.Shell(); err != nil { + return nil, err + } + + return &SSHClientSession{ + Stdin: stdin, + Stdout: comboWriter, + Session: sshSession, + }, nil +} + +// NewClientSFTP 创建SSH客户端SFTP对象 +func (c *ConnSSH) NewClientSFTP() (*SSHClientSFTP, error) { + sftpClient, err := gosftp.NewClient(c.Client) + if err != nil { + logger.Errorf("NewClientSFTP failed to create sftp: => %s", err.Error()) + return nil, err + } + + return &SSHClientSFTP{ + Client: sftpClient, + }, nil +} + +// NewClientByLocalPrivate 创建SSH客户端-本地私钥(~/.ssh/id_rsa)直连 +// +// ssh.ConnSSH{ +// User: "user", +// Addr: "192.168.x.x", +// Port: body.Port, +// } +func (c *ConnSSH) NewClientByLocalPrivate() (*ConnSSH, error) { + c.AuthMode = "1" + privateKey, err := c.CurrentUserRsaKey(false) + if err != nil { + return nil, err + } + c.PrivateKey = privateKey + return c.NewClient() +} + +// CurrentUserRsaKey 当前用户OMC使用的RSA私钥 +// 默认读取私钥 +// ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa +// ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub +func (c *ConnSSH) CurrentUserRsaKey(publicKey bool) (string, error) { + usr, err := user.Current() + if err != nil { + logger.Errorf("CurrentUserRsaKey get => %s", err.Error()) + return "", err + } + + // 是否存在私钥并创建 + keyPath := fmt.Sprintf("%s/.ssh/id_rsa", usr.HomeDir) + if _, err := os.Stat(keyPath); err != nil { + if _, err := cmd.ExecWithCheck("ssh-keygen", "-t", "rsa", "-P", "", "-f", keyPath); err != nil { + logger.Errorf("CurrentUserPrivateKey ssh-keygen [%s] rsa => %s", usr.Username, err.Error()) + } + } + + // 读取用户默认的文件 + if publicKey { + keyPath = keyPath + ".pub" + } + key, err := os.ReadFile(keyPath) + if err != nil { + logger.Errorf("CurrentUserRsaKey [%s] read => %s", usr.Username, err.Error()) + return "", fmt.Errorf("read file %s fail", keyPath) + } + return string(key), nil +} + +// SendToAuthorizedKeys 发送当前用户私钥到远程服务器进行授权密钥 +func (c *ConnSSH) SendToAuthorizedKeys() error { + publicKey, err := c.CurrentUserRsaKey(true) + if err != nil { + return err + } + // "sudo mkdir -p ~/.ssh && sudo chown omcuser:omcuser ~/.ssh && sudo chmod 700 ~/.ssh" + // "sudo touch ~/.ssh/authorized_keys && sudo chown omcuser:omcuser ~/.ssh/authorized_keys && sudo chmod 600 ~/.ssh/authorized_keys" + // "echo 'ssh-rsa AAAAB3= pc-host\n' | sudo tee -a ~/.ssh/authorized_keys" + authorizedKeysEntry := fmt.Sprintln(strings.TrimSpace(publicKey)) + cmdStrArr := []string{ + fmt.Sprintf("sudo mkdir -p ~/.ssh && sudo chown %s:%s ~/.ssh && sudo chmod 700 ~/.ssh", c.User, c.User), + fmt.Sprintf("sudo touch ~/.ssh/authorized_keys && sudo chown %s:%s ~/.ssh/authorized_keys && sudo chmod 600 ~/.ssh/authorized_keys", c.User, c.User), + fmt.Sprintf("echo '%s' | sudo tee -a ~/.ssh/authorized_keys", authorizedKeysEntry), + } + _, err = c.RunCMD(strings.Join(cmdStrArr, " && ")) + if err != nil { + logger.Errorf("SendAuthorizedKeys echo err %s", err.Error()) + return err + } + return nil +} diff --git a/src/framework/ssh/ssh_session.go b/src/framework/ssh/ssh_session.go new file mode 100644 index 00000000..1244ac2b --- /dev/null +++ b/src/framework/ssh/ssh_session.go @@ -0,0 +1,73 @@ +package ssh + +import ( + "bytes" + "fmt" + "io" + "sync" + + gossh "golang.org/x/crypto/ssh" +) + +// SSHClientSession SSH客户端会话对象 +type SSHClientSession struct { + Stdin io.WriteCloser + Stdout *singleWriter + Session *gossh.Session +} + +// Close 关闭会话 +func (s *SSHClientSession) Close() { + if s.Stdin != nil { + s.Stdin.Close() + } + if s.Stdout != nil { + s.Stdout = nil + } + if s.Session != nil { + s.Session.Close() + } +} + +// Write 写入命令 回车(\n)才会执行 +func (s *SSHClientSession) Write(cmd string) (int, error) { + if s.Stdin == nil { + return 0, fmt.Errorf("ssh client session is nil to content write failed") + } + return s.Stdin.Write([]byte(cmd)) +} + +// Read 读取结果 +func (s *SSHClientSession) Read() []byte { + if s.Stdout == nil { + return []byte{} + } + bs := s.Stdout.Bytes() + if len(bs) > 0 { + s.Stdout.Reset() + return bs + } + return []byte{} +} + +// singleWriter SSH客户端会话消息 +type singleWriter struct { + b bytes.Buffer + mu sync.Mutex +} + +func (w *singleWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.b.Write(p) +} +func (w *singleWriter) Bytes() []byte { + w.mu.Lock() + defer w.mu.Unlock() + return w.b.Bytes() +} +func (w *singleWriter) Reset() { + w.mu.Lock() + defer w.mu.Unlock() + w.b.Reset() +} diff --git a/src/framework/utils/file/excel.go b/src/framework/utils/file/excel.go index 69b7aa53..f4a1abf3 100644 --- a/src/framework/utils/file/excel.go +++ b/src/framework/utils/file/excel.go @@ -152,3 +152,112 @@ func WriteSheet(headerCells map[string]string, dataCells []map[string]any, fileN } return saveFilePath, nil } + +// WriterFileExecl 写入xlsx文件 +// +// 例如: +// headerCells := map[string]string{"A1": "姓名", "B1": "年龄", "C1": "城市"} +// +// dataCells := []map[string]any{ +// {"A2": "1", "B2": "2", "C2": "3"}, +// } +// +// filePath := "example.xlsx" +// err := file.WriterFileExecl(headerCells, dataCells, filePath, "Sheet1") +func WriterFileExecl(headerCells map[string]string, dataCells []map[string]any, filePath, sheetName string) error { + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + logger.Errorf("WriterFileExecl to close worksheet file: %v", err) + } + }() + + // 创建一个工作表 + if sheetName == "" { + sheetName = "Sheet1" + } + index, err := f.NewSheet(sheetName) + if err != nil { + return fmt.Errorf("failed to create worksheet %v", err) + } + // 设置工作簿的默认工作表 + f.SetActiveSheet(index) + + // 首个和最后key名称 + firstKey := "A" + lastKey := "B" + + // 第一行表头标题 + for key, title := range headerCells { + f.SetCellValue(sheetName, key, title) + if key[:1] > lastKey { + lastKey = key[:1] + } + } + + // 设置工作表上宽度为 20 + f.SetColWidth(sheetName, firstKey, lastKey, 20) + + // 从第二行开始的数据 + for _, cell := range dataCells { + for key, value := range cell { + f.SetCellValue(sheetName, key, value) + } + } + + // 创建文件目录 + if err := os.MkdirAll(filepath.Dir(filePath), 0775); err != nil { + return fmt.Errorf("failed to create save file %v", err) + } + + // 根据指定路径保存文件 + if err := f.SaveAs(filePath); err != nil { + return fmt.Errorf("failed to save worksheet %v", err) + } + return nil +} + +// ReadFileExecl 读取xlsx文件 转换数组数据 +func ReadFileExecl(filePath, sheetName string) ([]map[string]string, error) { + data := make([]map[string]string, 0) + // 打开 Excel 文件 + f, err := excelize.OpenFile(filePath) + if err != nil { + return data, err + } + defer func() { + if err := f.Close(); err != nil { + logger.Errorf("ReadSheet to close worksheet file : %v", err) + } + }() + + // 检查工作簿是否存在 + if sheetName == "" { + sheetName = "Sheet1" + } + if visible, _ := f.GetSheetVisible(sheetName); !visible { + return data, fmt.Errorf("failed to read workbook %s", sheetName) + } + + // 获取工作簿记录 + rows, err := f.GetRows(sheetName) + if err != nil { + return data, err + } + + for i, row := range rows { + // 跳过第一行 + if i == 0 { + continue + } + // 遍历每个单元格 + rowData := map[string]string{} + for columnIndex, cellValue := range row { + columnName, _ := excelize.ColumnNumberToName(columnIndex + 1) + rowData[columnName] = cellValue + } + + data = append(data, rowData) + } + return data, nil +} diff --git a/src/framework/utils/file/file.go b/src/framework/utils/file/file.go index 25ad2585..e4937dbc 100644 --- a/src/framework/utils/file/file.go +++ b/src/framework/utils/file/file.go @@ -12,7 +12,7 @@ import ( "time" "be.ems/src/framework/config" - "be.ems/src/framework/constants/uploadsubpath" + "be.ems/src/framework/constants" "be.ems/src/framework/logger" "be.ems/src/framework/utils/date" "be.ems/src/framework/utils/generate" @@ -237,7 +237,7 @@ func TransferChunkUploadFile(file *multipart.FileHeader, index, identifier strin // 上传资源路径 prefix, dir := resourceUpload() // 新文件名称并组装文件地址 - filePath := filepath.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + filePath := filepath.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) writePathFile := filepath.Join(dir, filePath, index) // 存入新文件路径 err = transferToNewFile(file, writePathFile) @@ -261,7 +261,7 @@ func ChunkCheckFile(identifier, originalFileName string) ([]string, error) { } // 上传资源路径 _, dir := resourceUpload() - dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) readPath := path.Join(dir, dirPath) fileList, err := getDirFileNameList(readPath) if err != nil { @@ -286,7 +286,7 @@ func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error // 上传资源路径 prefix, dir := resourceUpload() // 切片存放目录 - dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) readPath := path.Join(dir, dirPath) // 组合存放文件路径 fileName := generateFileName(originalFileName) diff --git a/src/framework/utils/file/files.go b/src/framework/utils/file/files.go new file mode 100644 index 00000000..9f45e47a --- /dev/null +++ b/src/framework/utils/file/files.go @@ -0,0 +1,78 @@ +package file + +import ( + "os" + "path/filepath" +) + +// FileListRow 文件列表行数据 +type FileListRow struct { + FileType string `json:"fileType"` // 文件类型 dir, file, symlink + FileMode string `json:"fileMode"` // 文件的权限 + LinkCount int64 `json:"linkCount"` // 硬链接数目 + Owner string `json:"owner"` // 所属用户 + Group string `json:"group"` // 所属组 + Size int64 `json:"size"` // 文件的大小 + ModifiedTime int64 `json:"modifiedTime"` // 最后修改时间,单位为秒 + FileName string `json:"fileName"` // 文件的名称 +} + +// 文件列表 +// search 文件名后模糊* +// +// return 行记录,异常 +func FileList(path, search string) ([]FileListRow, error) { + var rows []FileListRow + + // 构建搜索模式 + pattern := "*" + if search != "" { + pattern = search + pattern + } + + // 读取目录内容 + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + // 遍历目录项 + for _, entry := range entries { + // 匹配文件名 + matched, err := filepath.Match(pattern, entry.Name()) + if err != nil || !matched { + continue + } + + // 获取文件详细信息 + info, err := entry.Info() + if err != nil { + continue + } + + // 确定文件类型 + fileType := "file" + if info.IsDir() { + fileType = "dir" + } else if info.Mode()&os.ModeSymlink != 0 { + fileType = "symlink" + } + + // 获取系统特定的文件信息 + linkCount, owner, group := getFileInfo(info) + + // 组装文件信息 + rows = append(rows, FileListRow{ + FileMode: info.Mode().String(), + FileType: fileType, + LinkCount: linkCount, + Owner: owner, + Group: group, + Size: info.Size(), + ModifiedTime: info.ModTime().UnixMilli(), + FileName: entry.Name(), + }) + } + + return rows, nil +} diff --git a/src/framework/utils/file/files_unix.go b/src/framework/utils/file/files_unix.go new file mode 100644 index 00000000..e7fd8c79 --- /dev/null +++ b/src/framework/utils/file/files_unix.go @@ -0,0 +1,36 @@ +//go:build !windows +// +build !windows + +package file + +import ( + "fmt" + "os" + "os/user" + "syscall" +) + +// getFileInfo 获取系统特定的文件信息s +func getFileInfo(info os.FileInfo) (linkCount int64, owner, group string) { + // Unix-like 系统 (Linux, macOS) + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + // 获取用户名 + ownerName := "root" + if stat.Uid != 0 { + if u, err := user.LookupId(fmt.Sprint(stat.Uid)); err == nil { + ownerName = u.Username + } + } + + // 获取组名 + groupName := "root" + if stat.Gid != 0 { + if g, err := user.LookupGroupId(fmt.Sprint(stat.Gid)); err == nil { + groupName = g.Name + } + } + + return int64(stat.Nlink), ownerName, groupName + } + return 1, "", "" +} diff --git a/src/framework/utils/file/files_windows.go b/src/framework/utils/file/files_windows.go new file mode 100644 index 00000000..efaf414b --- /dev/null +++ b/src/framework/utils/file/files_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package file + +import ( + "os" +) + +// getFileInfo 获取系统特定的文件信息 +func getFileInfo(_ os.FileInfo) (linkCount int64, owner, group string) { + return 1, "Administrator", "Administrators" +} diff --git a/src/modules/common/common.go b/src/modules/common/common.go index 3bbbd97a..0a37ffdc 100644 --- a/src/modules/common/common.go +++ b/src/modules/common/common.go @@ -90,11 +90,18 @@ func Setup(router *gin.Engine) { // 文件操作处理 fileGroup := router.Group("/file") { - fileGroup.GET("/download/:filePath", middleware.PreAuthorize(nil), controller.NewFile.Download) - fileGroup.POST("/upload", middleware.PreAuthorize(nil), controller.NewFile.Upload) fileGroup.POST("/chunkCheck", middleware.PreAuthorize(nil), controller.NewFile.ChunkCheck) fileGroup.POST("/chunkUpload", middleware.PreAuthorize(nil), controller.NewFile.ChunkUpload) fileGroup.POST("/chunkMerge", middleware.PreAuthorize(nil), controller.NewFile.ChunkMerge) - fileGroup.POST("/transferStaticFile", middleware.PreAuthorize(nil), controller.NewCommont.TransferStaticFile) + + fileGroup.POST("/upload", middleware.PreAuthorize(nil), controller.NewFile.Upload) + fileGroup.POST("/chunk-check", middleware.PreAuthorize(nil), controller.NewFile.ChunkCheck) + fileGroup.POST("/chunk-upload", middleware.PreAuthorize(nil), controller.NewFile.ChunkUpload) + fileGroup.POST("/chunk-merge", middleware.PreAuthorize(nil), controller.NewFile.ChunkMerge) + fileGroup.GET("/download/:filePath", middleware.PreAuthorize(nil), controller.NewFile.Download) + fileGroup.GET("/list", middleware.PreAuthorize(nil), controller.NewFile.List) + fileGroup.GET("", middleware.PreAuthorize(nil), controller.NewFile.File) + fileGroup.DELETE("", middleware.PreAuthorize(nil), controller.NewFile.Remove) + fileGroup.POST("/transfer-static-file", middleware.PreAuthorize(nil), controller.NewFile.TransferStaticFile) } } diff --git a/src/modules/common/controller/file.go b/src/modules/common/controller/file.go index c09ead28..52ad5b87 100644 --- a/src/modules/common/controller/file.go +++ b/src/modules/common/controller/file.go @@ -4,18 +4,19 @@ import ( "encoding/base64" "fmt" "net/url" + "os" "path/filepath" + "runtime" "strings" "be.ems/src/framework/config" - "be.ems/src/framework/constants/uploadsubpath" + "be.ems/src/framework/constants" "be.ems/src/framework/i18n" - "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" "be.ems/src/framework/utils/file" - "be.ems/src/framework/vo/result" "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" ) // 实例化控制层 FileController 结构体 @@ -30,16 +31,16 @@ type FileController struct{} // // GET /download/:filePath func (s *FileController) Download(c *gin.Context) { - language := ctx.AcceptLanguage(c) + language := reqctx.AcceptLanguage(c) filePath := c.Param("filePath") if len(filePath) < 8 { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + c.JSON(422, resp.CodeMsg(422002, i18n.TKey(language, "app.common.err400"))) return } // base64解析出地址 decodedBytes, err := base64.StdEncoding.DecodeString(filePath) if err != nil { - c.JSON(400, result.CodeMsg(400, err.Error())) + c.JSON(422, resp.CodeMsg(422002, err.Error())) return } routerPath := string(decodedBytes) @@ -48,7 +49,7 @@ func (s *FileController) Download(c *gin.Context) { headerRange := c.GetHeader("Range") resultMap, err := file.ReadUploadFileStream(routerPath, headerRange) if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } @@ -83,39 +84,41 @@ func (s *FileController) Download(c *gin.Context) { // @Description Upload a file, interface param use // @Router /file/upload [post] func (s *FileController) Upload(c *gin.Context) { - language := ctx.AcceptLanguage(c) // 上传的文件 formFile, err := c.FormFile("file") if err != nil { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + c.JSON(422, resp.CodeMsg(422002, "bind err: file is empty")) return } - // 子路径 + // 子路径需要在指定范围内 subPath := c.PostForm("subPath") - if _, ok := uploadsubpath.UploadSubpath[subPath]; !ok { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + _, ok := constants.UPLOAD_SUB_PATH[subPath] + if subPath != "" && !ok { + c.JSON(422, resp.CodeMsg(422002, "bind err: subPath not in range")) return } + if subPath == "" { + subPath = constants.UPLOAD_COMMON + } // 上传文件转存 - upFilePath, err := file.TransferUploadFile(formFile, subPath, nil) + uploadFilePath, err := file.TransferUploadFile(formFile, subPath, []string{}) if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } - newFileName := upFilePath[strings.LastIndex(upFilePath, "/")+1:] - c.JSON(200, result.OkData(map[string]string{ - "url": "//" + c.Request.Host + upFilePath, - "fileName": upFilePath, - "newFileName": newFileName, + c.JSON(200, resp.OkData(map[string]string{ + "url": "//" + c.Request.Host + uploadFilePath, + "filePath": uploadFilePath, + "newFileName": filepath.Base(uploadFilePath), "originalFileName": formFile.Filename, })) } // 切片文件检查 // -// POST /chunkCheck +// POST /chunk-check // // @Tags common/file // @Accept json @@ -125,33 +128,30 @@ func (s *FileController) Upload(c *gin.Context) { // @Security TokenAuth // @Summary Slice file checking // @Description Slice file checking -// @Router /file/chunkCheck [post] +// @Router /file/chunk-check [post] func (s *FileController) ChunkCheck(c *gin.Context) { - language := ctx.AcceptLanguage(c) var body struct { - // 唯一标识 - Identifier string `json:"identifier" binding:"required"` - // 文件名 - FileName string `json:"fileName" binding:"required"` + Identifier string `json:"identifier" binding:"required"` // 唯一标识 + FileName string `json:"fileName" binding:"required"` // 文件名 } - err := c.ShouldBindJSON(&body) - if err != nil { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) return } // 读取标识目录 chunks, err := file.ChunkCheckFile(body.Identifier, body.FileName) if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } - c.JSON(200, result.OkData(chunks)) + c.JSON(200, resp.OkData(chunks)) } // 切片文件合并 // -// POST /chunkMerge +// POST /chunk-merge // // @Tags common/file // @Accept json @@ -161,46 +161,45 @@ func (s *FileController) ChunkCheck(c *gin.Context) { // @Security TokenAuth // @Summary Slice file merge // @Description Slice file merge -// @Router /file/chunkMerge [post] +// @Router /file/chunk-merge [post] func (s *FileController) ChunkMerge(c *gin.Context) { - language := ctx.AcceptLanguage(c) var body struct { - // 唯一标识 - Identifier string `json:"identifier" binding:"required"` - // 文件名 - FileName string `json:"fileName" binding:"required"` - // 子路径类型 - SubPath string `json:"subPath" binding:"required"` + Identifier string `json:"identifier" binding:"required"` // 唯一标识 + FileName string `json:"fileName" binding:"required"` // 文件名 + SubPath string `json:"subPath"` // 子路径类型 } - err := c.ShouldBindJSON(&body) - if err != nil { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) return } - if _, ok := uploadsubpath.UploadSubpath[body.SubPath]; !ok { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + // 子路径需要在指定范围内 + if _, ok := constants.UPLOAD_SUB_PATH[body.SubPath]; body.SubPath != "" && !ok { + c.JSON(422, resp.CodeMsg(422002, "bind err: subPath not in range")) return } + if body.SubPath == "" { + body.SubPath = constants.UPLOAD_COMMON + } // 切片文件合并 mergeFilePath, err := file.ChunkMergeFile(body.Identifier, body.FileName, body.SubPath) if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } - newFileName := mergeFilePath[strings.LastIndex(mergeFilePath, "/")+1:] - c.JSON(200, result.OkData(map[string]string{ + c.JSON(200, resp.OkData(map[string]string{ "url": "//" + c.Request.Host + mergeFilePath, - "fileName": mergeFilePath, - "newFileName": newFileName, + "filePath": mergeFilePath, + "newFileName": filepath.Base(mergeFilePath), "originalFileName": body.FileName, })) } // 切片文件上传 // -// POST /chunkUpload +// POST /chunk-upload // // @Tags common/file // @Accept multipart/form-data @@ -212,41 +211,214 @@ func (s *FileController) ChunkMerge(c *gin.Context) { // @Security TokenAuth // @Summary Sliced file upload // @Description Sliced file upload -// @Router /file/chunkUpload [post] +// @Router /file/chunk-upload [post] func (s *FileController) ChunkUpload(c *gin.Context) { - language := ctx.AcceptLanguage(c) // 切片编号 index := c.PostForm("index") // 切片唯一标识 identifier := c.PostForm("identifier") + if index == "" || identifier == "" { + c.JSON(422, resp.CodeMsg(422002, "bind err: index and identifier must be set")) + return + } // 上传的文件 formFile, err := c.FormFile("file") - if index == "" || identifier == "" || err != nil { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + if err != nil { + c.JSON(422, resp.CodeMsg(422002, "bind err: file is empty")) return } // 上传文件转存 chunkFilePath, err := file.TransferChunkUploadFile(formFile, index, identifier) if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } - c.JSON(206, result.OkData(chunkFilePath)) + c.JSON(206, resp.OkData(chunkFilePath)) +} + +// 本地文件列表 +// +// GET /list +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param pageNum query number true "pageNum" default(1) +// @Param pageSize query number true "pageSize" default(10) +// @Param search query string false "search prefix" default(upf) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local file list +// @Description Local file list +// @Router /file/list [get] +func (s *FileController) List(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + PageNum int64 `form:"pageNum" binding:"required"` + PageSize int64 `form:"pageSize" binding:"required"` + Search string `form:"search"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) + return + } + + // 获取文件列表 + localFilePath := querys.Path + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + rows, err := file.FileList(localFilePath, querys.Search) + if err != nil { + c.JSON(200, resp.OkData(map[string]any{ + "path": querys.Path, + "total": len(rows), + "rows": []file.FileListRow{}, + })) + return + } + + // 对数组进行切片分页 + lenNum := int64(len(rows)) + start := (querys.PageNum - 1) * querys.PageSize + end := start + querys.PageSize + var splitRows []file.FileListRow + if start >= lenNum { + splitRows = []file.FileListRow{} + } else if end >= lenNum { + splitRows = rows[start:] + } else { + splitRows = rows[start:end] + } + + c.JSON(200, resp.OkData(map[string]any{ + "path": querys.Path, + "total": lenNum, + "rows": splitRows, + })) +} + +// 本地文件获取下载 +// +// DELETE / +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param fileName query string true "file name" default(omc.log) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local files for download +// @Description Local files for download +// @Router /file [get] +func (s *FileController) File(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + Filename string `form:"fileName" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) + return + } + + // 检查路径是否在允许的目录范围内 + allowedPaths := []string{"/var/log", "/tmp", "/usr/local/omc/backup"} + allowed := false + for _, p := range allowedPaths { + if strings.HasPrefix(querys.Path, p) { + allowed = true + break + } + } + if !allowed { + c.JSON(200, resp.ErrMsg("operation path is not within the allowed range")) + return + } + + // 获取文件路径并下载 + localFilePath := filepath.Join(querys.Path, querys.Filename) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + if _, err := os.Stat(localFilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg("file does not exist")) + return + } + c.FileAttachment(localFilePath, querys.Filename) +} + +// 本地文件删除 +// +// DELETE / +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param fileName query string true "file name" default(omc.log) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local file deletion +// @Description Local file deletion +// @Router /file [delete] +func (s *FileController) Remove(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + Filename string `form:"fileName" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) + return + } + + // 检查路径是否在允许的目录范围内 + allowedPaths := []string{"/tmp", "/usr/local/omc/backup"} + allowed := false + for _, p := range allowedPaths { + if strings.HasPrefix(querys.Path, p) { + allowed = true + break + } + } + if !allowed { + c.JSON(200, resp.ErrMsg("operation path is not within the allowed range")) + return + } + + // 获取文件路径并删除 + localFilePath := filepath.Join(querys.Path, querys.Filename) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + if _, err := os.Stat(localFilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg("file does not exist")) + return + } + if err := os.Remove(localFilePath); err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + c.JSON(200, resp.Ok(nil)) } // 转存指定对应文件到静态目录 // -// POST /transferStaticFile -func (s *CommontController) TransferStaticFile(c *gin.Context) { - language := ctx.AcceptLanguage(c) +// POST /transfer-static-file +func (s *FileController) TransferStaticFile(c *gin.Context) { var body struct { UploadPath string `json:"uploadPath" binding:"required"` StaticPath string `json:"staticPath" binding:"required"` Language string `json:"language" binding:"required"` } - if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) return } @@ -257,7 +429,7 @@ func (s *CommontController) TransferStaticFile(c *gin.Context) { static := config.Get("staticFile.default").(map[string]any) dir, err := filepath.Abs(static["dir"].(string)) if err != nil { - c.JSON(400, result.CodeMsg(400, err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } @@ -267,10 +439,10 @@ func (s *CommontController) TransferStaticFile(c *gin.Context) { err = file.CopyUploadFile(body.UploadPath, newFile) if err != nil { - c.JSON(400, result.CodeMsg(400, err.Error())) + c.JSON(200, resp.ErrMsg(err.Error())) return } urlPath := strings.Replace(newFile, dir, static["prefix"].(string), 1) - c.JSON(200, result.OkData(filepath.ToSlash(urlPath))) + c.JSON(200, resp.OkData(filepath.ToSlash(urlPath))) } diff --git a/src/modules/network_data/controller/all_backup.go b/src/modules/network_data/controller/all_backup.go new file mode 100644 index 00000000..3b4d4c55 --- /dev/null +++ b/src/modules/network_data/controller/all_backup.go @@ -0,0 +1,101 @@ +package controller + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/service" + + "github.com/gin-gonic/gin" +) + +// 实例化控制层 BackupController 结构体 +var NewBackup = &BackupController{ + backupService: service.NewBackup, +} + +// 备份数据 +// +// PATH /backup +type BackupController struct { + backupService *service.Backup // 备份相关服务 +} + +// 备份文件-更新FTP配置 +// +// PUT /ftp +func (s BackupController) FTPUpdate(c *gin.Context) { + var body model.BackupDataFTP + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) + return + } + + byteData, err := json.Marshal(body) + if err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + + up := s.backupService.FTPConfigUpdate(string(byteData), reqctx.LoginUserToUserName(c)) + if up <= 0 { + c.JSON(200, resp.ErrMsg("update failed")) + return + } + c.JSON(200, resp.Ok(nil)) +} + +// 备份文件-获取FTP配置 +// +// GET /ftp +func (s BackupController) FTPInfo(c *gin.Context) { + info := s.backupService.FTPConfigInfo() + c.JSON(200, resp.OkData(info)) +} + +// 备份文件-文件FTP发送 +// +// POST /ftp +func (s BackupController) FTPPush(c *gin.Context) { + var body struct { + Path string `form:"path" binding:"required"` // 路径必须是 BACKUP_DIR 开头的路径 + Filename string `form:"fileName" binding:"required"` + Tag string `form:"tag" binding:"required"` // 标签,用于区分不同的备份文件 + } + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(422001, errMsgs)) + return + } + // 判断路径是否合法 + if !strings.HasPrefix(body.Path, s.backupService.BACKUP_DIR) { + c.JSON(200, resp.ErrMsg("operation path is not within the allowed range")) + return + } + + // 判断文件是否存在 + localFilePath := filepath.Join(body.Path, body.Filename) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + if _, err := os.Stat(localFilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg("file does not exist")) + return + } + + // 发送文件 + err := s.backupService.FTPPushFile(localFilePath, body.Tag) + if err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + c.JSON(200, resp.Ok(nil)) +} diff --git a/src/modules/network_data/model/backup.go b/src/modules/network_data/model/backup.go new file mode 100644 index 00000000..2b51e687 --- /dev/null +++ b/src/modules/network_data/model/backup.go @@ -0,0 +1,11 @@ +package model + +// BackupDataFTP 备份数据FTP服务参数结构体 +type BackupDataFTP struct { + Password string `json:"password"` // FTP密码 + Username string `json:"username" binding:"required"` // FTP用户名 + ToIp string `json:"toIp" binding:"required"` // FTP服务器IP + ToPort int64 `json:"toPort" binding:"required"` // FTP服务器端口 + Dir string `json:"dir" binding:"required"` // FTP服务器目录 + Enable bool `json:"enable"` // 是否启用 +} diff --git a/src/modules/network_data/service/backup.go b/src/modules/network_data/service/backup.go new file mode 100644 index 00000000..a85744c2 --- /dev/null +++ b/src/modules/network_data/service/backup.go @@ -0,0 +1,86 @@ +package service + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "be.ems/src/framework/ssh" + "be.ems/src/modules/network_data/model" + neService "be.ems/src/modules/network_element/service" + systemService "be.ems/src/modules/system/service" +) + +// 实例化数据层 Backup 结构体 +var NewBackup = &Backup{ + BACKUP_DIR: "/usr/local/omc/backup", + neInfoService: neService.NewNeInfo, + sysConfigService: systemService.NewSysConfigImpl, +} + +// Backup 备份相关 服务层处理 +type Backup struct { + BACKUP_DIR string // 备份目录 + neInfoService *neService.NeInfo // 网元信息服务 + sysConfigService *systemService.SysConfigImpl // 参数配置服务 +} + +// FTPConfigUpdate 更新FTP配置信息 +func (r Backup) FTPConfigUpdate(value, updateBy string) int64 { + cfg := r.sysConfigService.SelectConfigByKey("neData.backupDataFTP") + if cfg.ConfigID == "" { + return 0 + } + cfg.ConfigValue = value + cfg.UpdateBy = updateBy + return r.sysConfigService.UpdateEncryptValue(cfg) +} + +// FTPConfigInfo 获取FTP配置信息 +func (r Backup) FTPConfigInfo() model.BackupDataFTP { + info := model.BackupDataFTP{} + // 获取配置 + cfg := r.sysConfigService.FindByKeyDecryptValue("neData.backupDataFTP") + if cfg.ConfigID != "" && cfg.ConfigValue != "" { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &info); err != nil { + return info + } + } + return info +} + +// FTPPushFile 推送文件到FTP +func (r Backup) FTPPushFile(localFilePath, tag string) error { + cfgData := r.FTPConfigInfo() + if !cfgData.Enable { + return fmt.Errorf("setting remote backup ftp is disabled") + } + + connSSH := ssh.ConnSSH{ + User: cfgData.Username, + Password: cfgData.Password, + Addr: cfgData.ToIp, + Port: cfgData.ToPort, + AuthMode: "0", + } + sshClient, err := connSSH.NewClient() + if err != nil { + return err + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return err + } + defer sftpClient.Close() + + remotePath := strings.Replace(localFilePath, r.BACKUP_DIR, tag, 1) + remotePath = filepath.Join(cfgData.Dir, remotePath) + // 复制到远程 + if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil { + return fmt.Errorf("error uploading file") + } + return nil +} diff --git a/src/modules/system/service/sys_config.impl.go b/src/modules/system/service/sys_config.impl.go index c7c6d6c9..901a75b1 100644 --- a/src/modules/system/service/sys_config.impl.go +++ b/src/modules/system/service/sys_config.impl.go @@ -3,8 +3,10 @@ package service import ( "fmt" + "be.ems/src/framework/config" "be.ems/src/framework/constants/cachekey" "be.ems/src/framework/database/redis" + "be.ems/src/framework/utils/crypto" "be.ems/src/modules/system/model" "be.ems/src/modules/system/repository" ) @@ -168,3 +170,40 @@ func (r *SysConfigImpl) SelectConfigByKey(configKey string) model.SysConfig { } return model.SysConfig{} } + +// FindByKey 查询配置信息BY键 +func (s SysConfigImpl) FindByKey(configKey string) model.SysConfig { + sysConf := s.sysConfigRepository.SelectConfigList(model.SysConfig{ + ConfigKey: configKey, + }) + if len(sysConf) > 0 { + return sysConf[0] + } + return model.SysConfig{} +} + +// UpdateEncryptValue 更新并加密配置值信息 +func (s SysConfigImpl) UpdateEncryptValue(sysConfig model.SysConfig) int64 { + appKey := config.Get("aes.appKey").(string) + bodyEn, err := crypto.AESEncryptBase64(sysConfig.ConfigValue, appKey) + if err != nil { + return 0 + } + sysConfig.ConfigValue = bodyEn + return s.UpdateConfig(sysConfig) +} + +// FindByKeyDecryptValue 获取并解密配置值信息 +func (s SysConfigImpl) FindByKeyDecryptValue(configKey string) model.SysConfig { + item := s.FindByKey(configKey) + if item.ConfigKey != configKey { + return item + } + appKey := config.Get("aes.appKey").(string) + bodyDe, err := crypto.AESDecryptBase64(item.ConfigValue, appKey) + if err != nil { + return item + } + item.ConfigValue = bodyDe + return item +}