diff --git a/server/api/v1/cloud/server.go b/server/api/v1/cloud/server.go index 76d3b842e6..4f00bc13e3 100644 --- a/server/api/v1/cloud/server.go +++ b/server/api/v1/cloud/server.go @@ -1,41 +1,157 @@ package cloud import ( + "archive/zip" "context" "github.com/flipped-aurora/gin-vue-admin/server/constant" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/cloud" "github.com/flipped-aurora/gin-vue-admin/server/model/cloud/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" "io" + "os" + "os/exec" + "path/filepath" "time" ) type ServerApi struct{} -// 环境检测 -func (s *ServerApi) Check(c *gin.Context) { +// 整体为使用docker-compose部署项目 +// 前端使用nginx容器,nginx.conf需要映射到本地文件 +// 打包待发布的项目,先找到前端目录,在目录下执行npm run build,获得dist目录 +// 然后到后端文件下,根据传入的不同的system系统,设置环境变量,执行go build -o server +// windows变量 GOOS=windows GOARCH=amd64 +// linux变量 GOOS=linux GOARCH=amd64 +// mac变量 GOOS=darwin GOARCH=amd64 +// 然后将前端dist目录和后端server文件+ 后端resource + 后端 config.yaml 打包成zip文件 +// 根据config.yaml文件中的system的数据库类型设置和数据库具体配置来创建docker-compose.yml文件 +// 如果数据库地址不为127.0.0.1,则需要在docker-compose.yml文件中设置数据库地址 +// 如果数据库地址为127.0.0.1,则需要创建mysql容器,并且在docker-compose.yml文件中设置数据库地址,且挂盘数据到本地 +// 然后将zip文件传输到服务器上,解压到服务器/www-gva目录下 +// 进入/www-gva目录下,执行docker-compose up -d +func (s *ServerApi) Zip(c *gin.Context) { + var de request.Deploy + err := c.ShouldBindJSON(&de) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } - // 创建一个带有超时的上下文 - ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Hour) // 设置为1小时 - defer cancel() + webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "..") - // 将新的上下文传递给需要长时间运行的操作 - c.Request = c.Request.WithContext(ctx) + // 执行一个命令,获取错误输出 + webCmd := exec.Command("npm", "run", "build") + webCmd.Dir = webPath + webOut, err := webCmd.CombinedOutput() + if err != nil { + response.FailWithMessage(string(webOut), c) + return + } - checkDocker := cloud.CmdNode{ - Cmd: "docker version", + serverCmd := exec.Command("go", "build", "-o", "gin-vue-admin") + switch de.SystemType { + case "windows": + serverCmd.Env = append(os.Environ(), "GOOS=windows", "GOARCH=amd64") + case "linux": + serverCmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") + case "mac": + serverCmd.Env = append(os.Environ(), "GOOS=darwin", "GOARCH=amd64") } - checkDockerCompose := cloud.CmdNode{ - Cmd: "docker-compose version", + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + serverCmd.Dir = serverPath + serverOut, err := serverCmd.CombinedOutput() + + if err != nil { + response.FailWithMessage(string(serverOut), c) + return + } + + fileName := "deploy.zip" + // 创建一个新的zip文件 + zipFile, err := os.Create(fileName) + if err != nil { + response.FailWithMessage(err.Error(), c) + return } + defer zipFile.Close() - Run([]cloud.CmdNode{checkDocker, checkDockerCompose}, c) + // 创建一个zip写入器 + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + err = utils.DoZip(zipWriter, filepath.Join(webPath, "dist"), "dist") + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("打包成功", c) } -// 安装环境 -func (s *ServerApi) Install(c *gin.Context) { +func (s *ServerApi) Down(c *gin.Context) { + //Run([]cloud.CmdNode{checkDocker, checkDockerCompose}, c) +} + +func (s *ServerApi) Deploy(c *gin.Context) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + // 将新的上下文传递给需要长时间运行的操作 + var checkRequest request.SSHRequest + err := c.ShouldBindJSON(&checkRequest) + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } + + client, err := checkRequest.Connection() + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } + + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + + serverFile := filepath.Join(serverPath, "gin-vue-admin") + webFile := filepath.Join(serverPath, "deploy.zip") + + err = SftpUpload(serverFile, "/www-gva/server", "/www-gva/server/gin-vue-admin", client) + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } + err = SftpUpload(webFile, "/www-gva/web", "/www-gva/web/deploy.zip", client) + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } +} + +// 环境检测 +func (s *ServerApi) Check(c *gin.Context) { + + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + // 将新的上下文传递给需要长时间运行的操作 + var checkRequest request.SSHRequest + err := c.ShouldBindJSON(&checkRequest) + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } + + client, err := checkRequest.Connection() + if err != nil { + c.SSEvent(constant.FAIL, err.Error()) + return + } + defer client.Close() // 创建一个带有超时的上下文 ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Hour) // 设置为1小时 @@ -44,30 +160,26 @@ func (s *ServerApi) Install(c *gin.Context) { // 将新的上下文传递给需要长时间运行的操作 c.Request = c.Request.WithContext(ctx) - docker := cloud.CmdNode{ - Cmd: "curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh", + checkDocker := cloud.CmdNode{ + Cmd: "docker version", } - dockerCompose := cloud.CmdNode{ - Cmd: "sudo curl -L \"https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose && docker-compose version", + checkDockerCompose := cloud.CmdNode{ + Cmd: "docker-compose version", } - Run([]cloud.CmdNode{docker, dockerCompose}, c) + Run([]cloud.CmdNode{checkDocker, checkDockerCompose}, client, c) } -// 发布 -func (s *ServerApi) Deploy() { - -} +// 安装环境 +func (s *ServerApi) Install(c *gin.Context) { -func Run(cmds []cloud.CmdNode, c *gin.Context) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") // 将新的上下文传递给需要长时间运行的操作 var checkRequest request.SSHRequest - msg := make(chan cloud.MsgInfo, 100) err := c.ShouldBindJSON(&checkRequest) if err != nil { c.SSEvent(constant.FAIL, err.Error()) @@ -81,6 +193,28 @@ func Run(cmds []cloud.CmdNode, c *gin.Context) { } defer client.Close() + // 创建一个带有超时的上下文 + ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Hour) // 设置为1小时 + defer cancel() + + // 将新的上下文传递给需要长时间运行的操作 + c.Request = c.Request.WithContext(ctx) + + docker := cloud.CmdNode{ + Cmd: "curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh", + } + + dockerCompose := cloud.CmdNode{ + Cmd: "sudo curl -L \"https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose && docker-compose version", + } + + Run([]cloud.CmdNode{docker, dockerCompose}, client, c) + +} + +func Run(cmds []cloud.CmdNode, client *ssh.Client, c *gin.Context) { + msg := make(chan cloud.MsgInfo, 100) + go func() { c.Stream(func(w io.Writer) bool { select { @@ -103,3 +237,45 @@ func Run(cmds []cloud.CmdNode, c *gin.Context) { msg <- cloud.MsgInfo{Msg: "complete", Status: constant.COMPLETE} } + +func SftpUpload(localFile string, dir, remoteFile string, client *ssh.Client) error { + sftpClient, err := sftp.NewClient(client) + if err != nil { + return err + } + defer sftpClient.Close() + // Open the source file + srcFile, err := os.Open(localFile) + if err != nil { + return err + } + defer srcFile.Close() + + //remoteFile所在目录不存在时候 创建目录 + + if _, err := sftpClient.Stat(dir); err != nil { + session, err := client.NewSession() + if err != nil { + return err + } + err = session.Run("mkdir -p " + dir) + if err != nil { + return err + } + session.Close() + } + + // Create the destination file + dstFile, err := sftpClient.Create(remoteFile) + if err != nil { + return err + } + defer dstFile.Close() + + // Copy the file + _, err = dstFile.ReadFrom(srcFile) + if err != nil { + return err + } + return nil +} diff --git a/server/go.mod b/server/go.mod index dc424b14f4..a5135396f0 100644 --- a/server/go.mod +++ b/server/go.mod @@ -21,6 +21,7 @@ require ( github.com/mojocn/base64Captcha v1.3.5 github.com/otiai10/copy v1.7.0 github.com/pkg/errors v0.9.1 + github.com/pkg/sftp v1.13.1 github.com/qiniu/api.v7/v7 v7.4.1 github.com/qiniu/qmgo v1.1.8 github.com/redis/go-redis/v9 v9.0.5 @@ -87,6 +88,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/server/go.sum b/server/go.sum index 51aec5f04c..6c7c040a1c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -281,6 +281,7 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -335,6 +336,7 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzL github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/server/model/cloud/request/deploy.go b/server/model/cloud/request/deploy.go new file mode 100644 index 0000000000..bbf03417fd --- /dev/null +++ b/server/model/cloud/request/deploy.go @@ -0,0 +1,6 @@ +package request + +type Deploy struct { + Origin string `json:"origin"` + SystemType string `json:"systemType"` +} diff --git a/server/router/cloud/server.go b/server/router/cloud/server.go index 0d950cb9ad..13be4cef93 100644 --- a/server/router/cloud/server.go +++ b/server/router/cloud/server.go @@ -14,5 +14,8 @@ func (e *ServerRouter) InitServerRouter(Router *gin.RouterGroup) { { customerRouter.POST("check", serverApi.Check) // 检测环境 customerRouter.POST("install", serverApi.Install) // 安装环境 + customerRouter.POST("zip", serverApi.Zip) // 打包测试 + customerRouter.POST("deploy", serverApi.Deploy) // 上传部署 + } } diff --git a/server/service/system/sys_auto_code.go b/server/service/system/sys_auto_code.go index fed7233672..8a68298432 100644 --- a/server/service/system/sys_auto_code.go +++ b/server/service/system/sys_auto_code.go @@ -903,74 +903,14 @@ func (autoCodeService *AutoCodeService) PubPlug(plugName string) (zipPath string defer zipWriter.Close() webHeaderName := filepath.Join(plugName, "web", "plugin", plugName) - err = autoCodeService.doZip(zipWriter, webPath, webHeaderName) + err = utils.DoZip(zipWriter, webPath, webHeaderName) if err != nil { return } serverHeaderName := filepath.Join(plugName, "server", "plugin", plugName) - err = autoCodeService.doZip(zipWriter, serverPath, serverHeaderName) + err = utils.DoZip(zipWriter, serverPath, serverHeaderName) if err != nil { return } return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil } - -/* -* - - zipWriter zip写入器 - serverPath 存储的路径 - headerName 写有zip的路径 - -* -*/ -func (autoCodeService *AutoCodeService) doZip(zipWriter *zip.Writer, serverPath, headerName string) (err error) { - // 遍历serverPath目录并将所有非隐藏文件添加到zip归档中 - err = filepath.Walk(serverPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // 跳过隐藏文件和目录 - if strings.HasPrefix(info.Name(), ".") { - return nil - } - - // 创建一个新的文件头 - header, err := zip.FileInfoHeader(info) - if err != nil { - return err - } - - // 将文件头的名称设置为文件的相对路径 - rel, _ := filepath.Rel(serverPath, path) - header.Name = filepath.Join(headerName, rel) - // 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中 - if info.IsDir() { - header.Name += "/" - } - // 将文件添加到zip归档中 - writer, err := zipWriter.CreateHeader(header) - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - // 打开文件并将其内容复制到zip归档中 - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(writer, file) - if err != nil { - return err - } - - return nil - }) - return err -} diff --git a/server/utils/zip.go b/server/utils/zip.go index e0075f4000..b16d8d1025 100644 --- a/server/utils/zip.go +++ b/server/utils/zip.go @@ -108,3 +108,63 @@ func ZipFiles(filename string, files []string, oldForm, newForm string) error { } return nil } + +/* +* + + zipWriter zip写入器 + serverPath 存储的路径 + headerName 写有zip的路径 + +* +*/ +func DoZip(zipWriter *zip.Writer, serverPath, headerName string) (err error) { + // 遍历serverPath目录并将所有非隐藏文件添加到zip归档中 + err = filepath.Walk(serverPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过隐藏文件和目录 + if strings.HasPrefix(info.Name(), ".") { + return nil + } + + // 创建一个新的文件头 + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // 将文件头的名称设置为文件的相对路径 + rel, _ := filepath.Rel(serverPath, path) + header.Name = filepath.Join(headerName, rel) + // 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中 + if info.IsDir() { + header.Name += "/" + } + // 将文件添加到zip归档中 + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + // 打开文件并将其内容复制到zip归档中 + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(writer, file) + if err != nil { + return err + } + + return nil + }) + return err +} diff --git a/web/src/api/cloud.js b/web/src/api/cloud.js index 3471ca3868..ab2554e1a2 100644 --- a/web/src/api/cloud.js +++ b/web/src/api/cloud.js @@ -1,8 +1,15 @@ import service from '@/utils/request' -export const serverCheck = (data) => { +export const zip = (data) => { return service({ - url: '/server/check', + url: '/server/zip', + method: 'POST', + data + }) +} +export const deploy = (data) => { + return service({ + url: '/server/deploy', method: 'POST', data }) diff --git a/web/src/view/cloud/public/public.vue b/web/src/view/cloud/public/public.vue index 5163262b1a..252ca65d92 100644 --- a/web/src/view/cloud/public/public.vue +++ b/web/src/view/cloud/public/public.vue @@ -1,34 +1,62 @@