feat: 文件备份/CDR/LOG本地文件列表功能接口
This commit is contained in:
103
src/framework/ssh/files.go
Normal file
103
src/framework/ssh/files.go
Normal file
@@ -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
|
||||
}
|
||||
157
src/framework/ssh/sftp.go
Normal file
157
src/framework/ssh/sftp.go
Normal file
@@ -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
|
||||
}
|
||||
229
src/framework/ssh/ssh.go
Normal file
229
src/framework/ssh/ssh.go
Normal file
@@ -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
|
||||
}
|
||||
73
src/framework/ssh/ssh_session.go
Normal file
73
src/framework/ssh/ssh_session.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
78
src/framework/utils/file/files.go
Normal file
78
src/framework/utils/file/files.go
Normal file
@@ -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
|
||||
}
|
||||
36
src/framework/utils/file/files_unix.go
Normal file
36
src/framework/utils/file/files_unix.go
Normal file
@@ -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, "", ""
|
||||
}
|
||||
13
src/framework/utils/file/files_windows.go
Normal file
13
src/framework/utils/file/files_windows.go
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <fileName>
|
||||
// @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)))
|
||||
}
|
||||
|
||||
101
src/modules/network_data/controller/all_backup.go
Normal file
101
src/modules/network_data/controller/all_backup.go
Normal file
@@ -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))
|
||||
}
|
||||
11
src/modules/network_data/model/backup.go
Normal file
11
src/modules/network_data/model/backup.go
Normal file
@@ -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"` // 是否启用
|
||||
}
|
||||
86
src/modules/network_data/service/backup.go
Normal file
86
src/modules/network_data/service/backup.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user