golang实现文件发布脚本

此脚本可配合jenkins或者shell脚本来执行

原理:分为客户端和服务端方式,客户端调用服务端接口来上传文件到服务端,然后服务端获取到文件的md5返回给客户端,客户端也要获取本地的md5来进行比对,如果一样,说明传递过程文件没损失,然后客户端再次调用接口把文件的名字传递过去,服务端获取到名字后,把这个名字传递给服务端上面的shell脚本,来执行备份和发布操作。

deployagent.go内容如下:

package main

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	// "time"
)

type jtos struct {
	Code int    `json: code` //结构体标签加和不加,影响不大
	Data string `json: data`
}

// 记录日志信息
func loginfo(info string,value interface{}) {
	file, err := os.OpenFile("output.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal("无法打开日志文件:", err)
	}
	defer file.Close()

	// 设置日志输出到文件
	log.SetOutput(file)

	// 现在所有log打印都会写入文件
	log.Println(fmt.Sprintf("%s: %#v", info, value))
}

// 将json反序列化到结构体中,为了获取json中的data内容,也就是远程文件的md5值
func jsonToStruct(j string) string {
	jts := jtos{}
	err := json.Unmarshal([]byte(j), &jts)
	if err != nil {
		log.Fatal("JSON反序列化到结构体报错:", err)
	}
	return jts.Data

}
func md5Value(filename string) string {
	f, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()
	hash := md5.New()
	if _, err := io.Copy(hash, f); err != nil {
		log.Fatal(err)
	}
	hashBytes := hash.Sum(nil)
	hashString := hex.EncodeToString(hashBytes)
	return hashString

}
func dproject(filename string) {
	loginfo("文件名为:", filename)
	client := &http.Client{}          //创建一个http的client实例,用于发送http请求
	bf := &bytes.Buffer{}             //创建Buffer实例,用于在内存中存储发送的nultipart数据
	writer := multipart.NewWriter(bf) //创建writer实例,写入到buffer中,writer后续用于构建multipart/form-data请求体
	req, err := http.NewRequest("POST", "http://update.abc.com/upload", nil)
	if err != nil {
		log.Println("构建Request报错:", err)
		os.Exit(-1)
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())
	md5value := md5Value(filename)
	loginfo("本地文件的md5值为:", md5value)
	file, err := os.Open(filename)
	if err != nil {
		loginfo("打开文件报错", err)
		os.Exit(-1)
	}
	defer file.Close()
	//创建表单文件字段,字段名为filename,filepath.Base可以截取路径最后的文件名
	part, err := writer.CreateFormFile("filename", filepath.Base(filename))
	if err != nil {
		log.Fatal(err)
	}
	//将文件数据流复制到表单字段part中
	_, err = io.Copy(part, file)
	if err != nil {
		log.Fatal(err)
	}
	err = writer.Close()
	if err != nil {
		log.Fatal(err)
	}
	//writer.WriteField("fieldName","fieldValue")  //添加其他字段
	req.Body = io.NopCloser(bf)
	resp, err := client.Do(req)
	if err != nil {
		loginfo("请求远程服务器发送文件报错:", err)
		os.Exit(-1)
	}
	defer resp.Body.Close()
	if resp.StatusCode == 200 {
		bodys, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			loginfo("读取响应体报错", err)
			os.Exit(-1)
		}
		r := jsonToStruct(string(bodys))
		//发送之前的文件和发送到远程机器文件的md5值相同,说明文件传输正常
		if md5value == r {
			f1 := filepath.Ext(filepath.Base(filename))                                    //获取拓展名
			data := fmt.Sprintf("fname=%s", strings.Split(filepath.Base(filename), f1)[0]) //切割拓展名前面部分,包括.
			loginfo("传递到服务端的参数文件名为:", data)
			requestBody := bytes.NewBuffer([]byte(data))
			reqq, err := http.NewRequest("POST", "http://update.abc.com/fparam", requestBody)
			if err != nil {
				loginfo("传参到远程服务器报错", err)
				os.Exit(-1)
			}
			reqq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			resps, err := client.Do(reqq)
			if err != nil {
				loginfo("请求远程服务器并传参报错", err)
				os.Exit(-1)
			}
			defer reqq.Body.Close()
			bodyss, err := ioutil.ReadAll(resps.Body)
			if err != nil {
				loginfo("传递参数后读取响应体报错", err)
				os.Exit(-1)
			}
			loginfo("传递参数后读取的响应体内容为", string(bodyss))
		} else {
			loginfo("远程文件的md5值和本地的md5值不一样,请重新执行上传...", "")
			os.Exit(-1)
		}
	} else {
		loginfo("连接远程服务端异常:", resp)
	}
}
func main() {
	s := os.Args[1:]
	for _, v := range s {
		dproject(v)
	}
	// time.Sleep(time.Second * 10)
}

服务端脚本内容为:

package main

import (
	"crypto/md5"
	"encoding/hex"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"
)

// 获取文件的md5值
func md5Value(filename string) string {
	f, err := os.Open(filename)
	if err != nil {
		loginfo("获取文件md5值时打开文件报错", err)
		os.Exit(-1)
	}
	defer f.Close()
	hash := md5.New()
	if _, err := io.Copy(hash, f); err != nil {
		loginfo("执行io.copy出错", err)
		os.Exit(-1)
	}
	hashBytes := hash.Sum(nil)
	hashString := hex.EncodeToString(hashBytes)
	return hashString
}

func nginx(filename string, hostNode ...string) (string, error) {
	//hostNode是一个字符串切片
	str := ""
	for _, node := range hostNode {
		out, err := execSSH(node, fmt.Sprintf("/data/deploy/%s", filename), "/data/deploy", filename)
		if err != nil {
			return out, err
		}
		str = out
	}
	return str, nil
}

// 通过ssh方式
func execSSH(hostname, srcpath, destpath, filename string) (string, error) {
	str := ""
	//文件通过/upload上传到后默认就是到达nginx_45的,因此如果要将文件发布到nginx_45(本机),就不需要使用scp和ssh -t了,直接在本机执行即可
	if strings.TrimSpace(hostname) == "192.168.10.45" {
		backSlice := []string{"/data/deploy/scripts/backup.sh"}
		deploySlice := []string{"/data/deploy/scripts/deploy.sh"}
		mapp := make(map[int][]string)
		mapp[0] = backSlice
		mapp[1] = deploySlice
		for _, v := range mapp {
			loginfo("开始执行nginx_45备份与发布脚本:", v)
			loginfo("传递过来的文件名为", filename)
			cmd := exec.Command("bash", v[0], filename)
			out, err := cmd.Output()
			if err != nil {
				return fmt.Sprintf("执行nginx_45节点报错:%s", string(out)), err
			}
			str = string(out)
		}
		return str, nil
	} else if strings.TrimSpace(hostname) == "192.168.10.44" {
		loginfo("开始执行nginx_44服务上的备份与发布脚本......","")
		//	scpSlice := []string{fmt.Sprintf("%s", srcpath), fmt.Sprintf("cyyzops@%s:%s", hostname, destpath)}
		backSlice := []string{fmt.Sprintf("root@%s", hostname), fmt.Sprintf("/data/deploy/scripts/backup.sh %s", filename)}
		deploySlice := []string{fmt.Sprintf("root@%s", hostname), fmt.Sprintf("/data/deploy/scripts/deploy.sh %s", filename)}
		mapp := make(map[int][]string)
		mapp[0] = backSlice
		mapp[1] = deploySlice
		for _, v := range mapp {
			fmt.Println("要执行的远程脚本为:", v)
			cmd := exec.Command("ssh", v...)
			out, err := cmd.Output()
			if err != nil {
				return fmt.Sprintf("执行nginx_44节点报错:%s", string(out)), err
			}
			str = string(out)
		}
		return str, nil
	} else {
		return str, nil
	}
	return str, nil
}

// 通过ansible方式,跟上面的通过scp和ssh方式一样,根据情况选择
//
//	func execAnsible(hostname, srcpath, destpath string) error {
//		cmdSlice := [][]string{
//			{fmt.Sprintf("%s", hostname), "-m", "copy", "-a", fmt.Sprintf("src=%s dest=%s", srcpath, destpath)},
//			{fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash /data/script/backup.sh"},
//			{fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash /data/script/deploy.sh"},
//		}
//		for _, v := range cmdSlice {
//			//v是一个一维切片,...可以将切片中元素拆出来作为参数传递给Command,Command至少需要两个参数
//			cmd := exec.Command("ansible", v...)
//			_, err := cmd.CombinedOutput()
//			if err != nil {
//				fmt.Println("errrs:", err)
//				return err
//			}
//			//		continue
//		}
//		return nil
//	}
func checkFilename(filename string) (string, error) {
	const (
		nginx_44 = "192.168.10.44"
		nginx_45 = "192.168.10.45"
		nginx_38 = "192.168.10.38"
		nginx_39 = "192.168.10.39"
	)
	if filename == "sign-sys-front-esignsys-new" {
		s, err := nginx(filename, nginx_44, nginx_45)
		if err != nil {
			return s, err
		}
		return s, nil
	} else if filename == "sign-sys-front-admin-esignsys" {
		s, err := nginx(filename, nginx_44, nginx_45)
		if err != nil {
			return s, err
		}
		return s, nil
	} else if filename == "sign-core-system-0.1.1-SNAPSHOTESIGNSYS" {
		s, err := nginx(filename, nginx_45)
		if err != nil {
			return s, err
		}
		return s, nil
	} else {
		return "", errors.New(fmt.Sprintf("不存在这个文件: %s", filename))
	}
}

// 记录日志信息
func loginfo(info string,value interface{}) {
        file, err := os.OpenFile("output.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
        if err != nil {
                log.Fatal("无法打开日志文件:", err)
        }
        defer file.Close()

        // 设置日志输出到文件
        log.SetOutput(file)

        // 现在所有log打印都会写入文件
        log.Println(fmt.Sprintf("%s: %#v", info, value))
}

func main() {
	router := gin.Default()
	router.POST("/upload", func(c *gin.Context) {
		_, file, err := c.Request.FormFile("filename")
		if err != nil {
			loginfo("上传文件接口出现错误", err)
			return
		}
		err = c.SaveUploadedFile(file, "/data/deploy/"+file.Filename)
		if err != nil {
			loginfo("文件保存到/data/deploy目录出现错误", err)
			return
		}
		//获取md5值后返回给客户端做对比
		md5v := md5Value(fmt.Sprintf("/data/deploy/%s", file.Filename))
		c.JSON(200, gin.H{
			"code": 200,
			"data": md5v,
		})
		fmt.Println("remote machine md5:", md5v)
	})
	//客户端传递文件名过来,根据文件名在服务端做文件copy和备份发布操作
	router.POST("/fparam", func(c *gin.Context) {
		filename := c.PostForm("fname")
		out, err := checkFilename(filename)
		if err != nil {
			c.JSON(200, gin.H{"code": 101, "data": out, "error": fmt.Sprintf("%s",err)})
		} else {
			c.JSON(200, gin.H{
				"code": 200, "data": out, "error": err})
		}
	})
	router.Run(":8888")
}

注:要根据实际请求对脚本进行修改

backup.sh内容如下:

#!/bin/bash
: '
  @func: 备份jar包,如果backup中有今天备份的,就不再备份(方便回滚),如果没有就备份
  @datetime: 20240929
  @author: gongguan
'
set -e
set -u
cd $(dirname $0)
dateT=$(date "+%Y%m%d-%H:%M:%S")
backup(){
    srcpath=$1
    #截取时间的年月日部分
    ymd=$(echo ${dateT} | awk -F "-" '{print $1}')
    filename=$2
    backupdir=/data/deploy/backup
    #判断/data/deploy/backup路径下是否有除了当前备份外的其他时间的文件,如果有,删除掉
    if find ${backupdir} -type f -name "${filename}*" | grep -q .;then
           for i in $(ls ${backupdir}/${filename}*)
           do
                bd=$(echo ${i} | awk -F "-" '{print $6}')
                if [[ "${bd}" != "${ymd}" ]];then
                       rm -rf ${i}
                fi
           done
    fi
    : '
      如果备份目录下找不到文件,说明没备份,那么就执行备份,否则不执行
      这样可以保证当天如果发版本多次但是只会备份一次
    '
    if ! find ${backupdir} -type f -name "${filename}*" |  grep -q .;then
        if [ $filename == "sign-sys-front-admin-esignsys" ] || [ $filename == "sign-sys-front-esignsys-new" ];then
    	    tar --force-local -zcf  ${backupdir}/$2-${dateT}.tgz --directory=${srcpath} .
        elif [ $filename == "sign-core-system-0.1.1-SNAPSHOTESIGNSYS" ];then
            filePath=/data/java/sign-sys-service_esignsys_bjsfj
            cp $filePath/sign-core-system-0.1.1-SNAPSHOT.jar ${backupdir}/sign-core-system-0.1.1-SNAPSHOTESIGNSYS-${dateT}.jar
            sleep 1
            rm -rf ${filePath}/sign-core-system-0.1.1-SNAPSHOT.jar
        fi
    fi
}
case $1 in 
	"sign-sys-front-esignsys-new")
        filePath=/data/nginx_proxy/html/sign-sys_html
	backup ${filePath} sign-sys-front-esignsys-new
	;;
        "sign-sys-front-admin-esignsys")
        filePath=/data/nginx_proxy/html/sign-sys_html
	backup ${filePath} sign-sys-front-admin-esignsys
	;;
        "sign-core-system-0.1.1-SNAPSHOTESIGNSYS")
        filePath=/data/java/sign-sys-service_esignsys_bjsfj
        backup ${filePath} sign-core-system-0.1.1-SNAPSHOTESIGNSYS
        ;;
        *)
	echo "ERROR: 备份脚本需要输入正确的参数!!!!"
esac

标签