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


