go walk 開發window界面,上傳文件到阿里雲oss -- 服務器端

前面我們完成了一個網頁端的上傳oss程序:https://blog.csdn.net/daily886/article/details/103366145

現在我們把前後端分離

前端使用walk開發,window界面 ,客戶端操作流程:https://blog.csdn.net/daily886/article/details/103432917

後臺服務器使用go接收圖片並上傳到oss

服務器端目錄結構如下: 


項目根目錄是 goossserver

goossserver的目錄結構

├── conf                           # 配置文件統一存放目錄
│   ├── config.yaml          # 配置文件
├── config                        # 專門用來處理配置和配置文件的Go package
│   └── config.go                 
├── handler                      # 控制輸出到瀏覽器
│   ├── handler.go
├── logs                           # 日誌記錄
├── model                        # 操作模型
│   ├── aliyunoss.go         # 阿里雲oss操作模型
├── router                         # 路由相關處理
│   ├── middleware           # API服務器用的是Gin Web框架,Gin中間件存放位置
│   │   ├── header.go        # Gin中間件
│   └── router.go               # 路由
├── service                       # 實際業務處理函數存放位置
│   └── hotupdate.go        # 熱重啓服務
│   └── service.go             # 業務處理函數
├── uploads                      # 上傳文件本地存儲目錄
├── main.go                      # Go程序唯一入口

下面,我們根據目錄結構,從上往下建立文件夾和文件
建立文件夾和文件 goossserver/conf/config.yaml ,config.yaml 內容如下:

common:
  #https://help.aliyun.com/document_detail/87712.html?spm=a2c4g.11186623.6.882.36c55837XaJBzg 阿里雲oss接口
  aliyunoss:
    accessid: XXX                # 阿里雲的accessid
    accesskey: XXX               # 阿里雲的accesskey
    endpoint: XXX                # 阿里雲的endpoint
    bucket: XXX                  # 阿里雲的 bucket名稱
    uploaddir: test              # 所有上傳文件都存在這個目錄下面
    domain: http://XXX.com              # 文件訪問域名
  server: #服務器配置
    runmode: debug               # 開發模式, debug, release, test
    addr: :6669                  # HTTP綁定端口
    name: ossserver              # API Server的名字
    url: :6669   # pingServer函數請求的API服務器的ip:port
    max_ping_count: 10           # pingServer函數嘗試的次數


建立文件夾和文件 goossserver/config/config.go ,config.yaml 內容如下:

package config

import (
	"github.com/spf13/viper"
	"time"
	"os"
	"log"
)

// LogInfo 初始化日誌配置
func LogInfo() {
	file := "./logs/" + time.Now().Format("2006-01-02") + ".log"
	logFile, _ := os.OpenFile(file,os.O_RDWR| os.O_CREATE| os.O_APPEND, 0755)
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.SetOutput(logFile)
}

// Init 讀取初始化配置文件
func Init() error {
	//初始化配置
	if err := Config();err != nil{
		return err
	}

	//初始化日誌
	LogInfo()
	return nil
}

// Config viper解析配置文件
func Config() error{
	viper.AddConfigPath("conf")
	viper.SetConfigName("config")
	if err := viper.ReadInConfig();err != nil{
		return err
	}
	return nil
}


建立文件夾和文件 goossserver/handler/handler.go ,handler.go 內容如下:

package handler

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

//返回json 格式
/*
param c *gin.Context		 gin上下文 必傳
param code					 int 代號 必傳
param message string		 提示語 必傳
param data ...interface{} 	 返回數據 選填
*/
func SendResponse(c *gin.Context,code int,message string,data ...interface{}){
	if data == nil{
		data = make([]interface{},0)
	}
	//總是返回http狀態ok
	c.JSON(http.StatusOK,Response{
		Code: code,
		Message:message,
		Data: data,
	})
}

//返回成功
func SendSuccess(c *gin.Context,message string,data ...interface{}){
	SendResponse(c,1,message,data)
}

//返回提示
func SendTips(c *gin.Context,message string,data ...interface{}){
	SendResponse(c,0,message,data)
}

//返回失敗
func SendFailure(c *gin.Context,message string,data ...interface{}){
	SendResponse(c,-1,message,data)
}


建立文件夾 goossserver/logs 權限爲777
建立文件夾和文件 goossserver/model/aliyunoss.go ,aliyunoss.go 內容如下:

package model

import (
	"fmt"
	"log"
	"strings"

	"github.com/spf13/viper"
	"github.com/aliyun/aliyun-oss-go-sdk/oss"
)

type OssStruct struct {
	Url string `json:"url"`
}

//sdk 版本
func Aliossversion()(version string){
	return oss.Version
}

//初始化oss服務
func Initserver()(client *oss.Client,err error){
	// Endpoint以杭州爲例,其它Region請按實際情況填寫。
	endpoint := viper.GetString("common.aliyunoss.endpoint")
	// 阿里雲主賬號AccessKey擁有所有API的訪問權限,風險很高。強烈建議您創建並使用RAM賬號進行API訪問或日常運維,請登錄 https://ram.console.aliyun.com 創建RAM賬號。
	accessKeyId := viper.GetString("common.aliyunoss.accessid")
	accessKeySecret := viper.GetString("common.aliyunoss.accesskey")
	// 創建OSSClient實例。
	client, err = oss.New(endpoint, accessKeyId, accessKeySecret)
	if err != nil{
		return
	}
	return
}

//獲取文件列表
func GetFilelist()(list []string,err error){
	list = make([]string,100)
	client,err := Initserver()
	// 獲取存儲空間。
	bucketName := viper.GetString("common.aliyunoss.bucket")
	bucket, err := client.Bucket(bucketName)
	if err != nil {
		return list,err
	}
	// 列舉文件。
	marker := ""
	for {
		lsRes, err := bucket.ListObjects(oss.Marker(marker))
		if err != nil {
			return list,err
		}
		// 打印列舉文件,默認情況下一次返回100條記錄。
		for _, object := range lsRes.Objects {
			log.Printf("object.Key:%v\n",object.Key)
			list = append(list,object.Key)
		}
		if lsRes.IsTruncated {
			marker = lsRes.NextMarker
		} else {
			break
		}
	}
	return list,err
}

//上傳文件
func UploadFile(localfile string,uploadfile string)(resultfile string,err error){
	resultfile = ""
	// 創建OSSClient實例。
	client,err := Initserver()

	bucketName := viper.GetString("common.aliyunoss.bucket")
	// <yourObjectName>上傳文件到OSS時需要指定包含文件後綴在內的完整路徑,例如abc/efg/123.jpg。
	uploaddir := viper.GetString("common.aliyunoss.uploaddir")
	uploadfile = strings.Trim(uploadfile,"/")
	objectName := fmt.Sprintf("%s/%s",uploaddir,uploadfile) //完整的oss路徑
	// <yourLocalFileName>由本地文件路徑加文件名包括後綴組成,例如/users/local/myfile.txt。
	localFileName := localfile
	// 獲取存儲空間。
	bucket, err := client.Bucket(bucketName)
	if err != nil {
		return
	}
	// 上傳文件。
	err = bucket.PutObjectFromFile(objectName, localFileName)
	if err != nil {
		return
	}
	resultfile = objectName
	return
}

建立文件夾和文件 goossserver/router/middleware/header.go ,header.go 內容如下:

package middleware

import (
	"net/http"
	"time"
	"github.com/gin-gonic/gin"
)

//無緩存頭部中間件 ,
//要來防止客戶端獲取已經緩存的響應信息
func NoCache(c *gin.Context){
	c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
	c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
	c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
	c.Next()
}

//選項中間件
//要來給預請求 終止並退出中間件 ,鏈接並結束請求
func Options(c *gin.Context){
	if c.Request.Method != "OPTIONS"{
		c.Next()
	}else{
		c.Header("Access-Control-Allow-Origin","*")
		c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
		c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
		c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
		c.Header("Content-Type", "application/json")
		c.AbortWithStatus(200)
	}
}

//安全中間件
//要來保障數據安全的頭部
func Secure(c *gin.Context){
	c.Header("Access-Control-Allow-Origin", "*")
	c.Header("X-Frame-Options", "DENY")
	c.Header("X-Content-Type-Options", "nosniff")
	c.Header("X-XSS-Protection", "1; mode=block")
	if c.Request.TLS != nil {
		c.Header("Strict-Transport-Security", "max-age=31536000")
	}

	//也可以考慮添加一個安全代理的頭部
	//c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}

建立文件夾和文件 goossserver/router/router.go ,router.go 內容如下:

package router

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"goossserver/service"
	"goossserver/router/middleware"
)

//初始化路由
func InitRouter(g *gin.Engine){
	middlewares := []gin.HandlerFunc{}
	//中間件
	g.Use(gin.Recovery())
	g.Use(middleware.NoCache)
	g.Use(middleware.Options)
	g.Use(middleware.Secure)
	g.Use(middlewares...)


	//404處理
	g.NoRoute(func(c *gin.Context){
		c.String(http.StatusNotFound,"該路徑不存在")
	})

	//健康檢查中間件
	g.POST("/ossupload",service.OssUpload)//上傳oss
}

建立文件夾和文件 goossserver/service/hotupdate.go ,hotupdate.go 內容如下:

package service

import (
	"context"
	"errors"
	"flag"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"time"
)

/************************** 熱重啓 ***************************/

var (
	listener net.Listener = nil

	graceful =  flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)


//監聽服務器
func Listenserver(server *http.Server){
	var err error

	//解析參數
	flag.Parse()

	//設置監聽的對象(新建或已存在的socket描述符)
	if *graceful {
		//子進程監聽父進程傳遞的 socket描述符
		log.Println("listening on the existing file descriptor 3")
		//子進程的 0 1 2 是預留給 標準輸入 標準輸出 錯誤輸出
		//因此傳遞的socket 描述符應該放在子進程的 3
		f := os.NewFile(3,"")
		listener,err = net.FileListener(f)
		log.Printf( "graceful-reborn  %v %v  %#v \n", f.Fd(), f.Name(), listener)
	}else{
		//啓動守護進程
		daemonProcce(1,1);
		//父進程監聽新建的 socket 描述符
		log.Println("listening on a new file descriptor")
		listener,err = net.Listen("tcp",server.Addr)
		log.Printf("Actual pid is %d\n", syscall.Getpid())

	}
	if err != nil{
		log.Fatalf("listener error: %v\n",err)
	}
	go func(){
		err = server.Serve(listener) //http
		//err = server.ServeTLS(listener,pemPath,keyPath) //https
		log.Printf("server.Serve err: %v\n",err)
		tcp,_ := listener.(*net.TCPListener)
		fd,_ := tcp.File()
		log.Printf( "first-boot  %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
	}()
	//監聽信號
	handleSignal(server)
	log.Println("signal end")
}

//處理信號
func handleSignal(server *http.Server){
	//把信號 賦值給 通道
	ch := make(chan os.Signal, 1)
	//監聽信號
	signal.Notify(ch, syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)
	//阻塞主進程, 不停的監聽系統信號
	for{
		//通道 賦值給 sig
		sig := <-ch
		log.Printf("signal receive: %v\n", sig)
		ctx,_ := context.WithTimeout(context.Background(),20*time.Second)
		switch sig{
		case syscall.SIGINT,syscall.SIGTERM:  //終止進程執行
			log.Println("shutdown")
			signal.Stop(ch)       //停止通道
			server.Shutdown(ctx)  //關閉服務器窗口
			log.Println("graceful shutdown")
			return
		case syscall.SIGUSR2:  //進程熱重啓
			log.Println("reload")
			err := reload()  //執行熱重啓
			if err != nil{
				log.Fatalf("listener error: %v\n",err)
			}
			//server.Shutdown(ctx)
			log.Println("graceful reload")
			return
		}
	}
}

//熱重啓
func reload() error{
	tl, ok := listener.(*net.TCPListener)
	if !ok {
		return errors.New("listener is not tcp listener")
	}
	//獲取socket描述符
	currentFD, err := tl.File()
	if err != nil {
		return err
	}
	//設置傳遞給子進程的參數(包含 socket描述符)
	args := []string{"-graceful"}
	//args = append(args, "-continue")
	cmd := exec.Command(os.Args[0],args...)
	cmd.Stdout = os.Stdout  //標準輸出
	cmd.Stderr = os.Stderr  //錯誤輸出
	cmd.ExtraFiles = []*os.File{currentFD} //文件描述符

	err = cmd.Start()
	log.Printf("forked new pid %v: \n",cmd.Process.Pid)
	if err != nil{
		return err
	}
	return nil
}
/*
我們在父進程執行 cmd.ExtraFiles = []*os.File{f} 來傳遞 socket 描述符給子進程,子進程通過執行 f := os.NewFile(3, "") 來獲取該描述符。值得注意的是,子進程的 0 、1 和 2 分別預留給標準輸入、標準輸出和錯誤輸出,所以父進程傳遞的 socket 描述符在子進程的順序是從 3 開始。
*/


//nochdir 是 程序初始路徑 1是當前路徑,0是系統根目錄
//noclose 是 錯誤信息輸出 1是輸出當前, 0是不顯示錯誤信息
func daemonProcce(nochdir, noclose int) (int,error){
	// already a daemon
	log.Printf("syscall.Getppid() %+v\n",syscall.Getppid())
	//如果是守護進程 syscall.Getppid() = 1
	if syscall.Getppid() == 1 {
		/* Change the file mode mask */
		syscall.Umask(0)

		if nochdir == 0 {
			os.Chdir("/")
		}

		return 0, nil
	}

	files := make([]*os.File, 3, 6)
	if noclose == 0 {
		nullDev, err := os.OpenFile("/dev/null", 0, 0)
		if err != nil {
			return 1, err
		}
		files[0], files[1], files[2] = nullDev, nullDev, nullDev
	} else {
		files[0], files[1], files[2] = os.Stdin, os.Stdout, os.Stderr
	}

	dir, _ := os.Getwd()
	sysattrs := syscall.SysProcAttr{Setsid: true}
	attrs := os.ProcAttr{Dir: dir, Env: os.Environ(), Files: files, Sys: &sysattrs}

	proc, err := os.StartProcess(os.Args[0], os.Args, &attrs)
	if err != nil {
		return -1, err
	}
	proc.Release()
	os.Exit(0)

	return 0, nil
}

建立文件夾和文件 goossserver/service/service.go ,service.go 內容如下:

package service

import (
	"log"
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"

	. "goossserver/handler"
	"goossserver/model"
)


func OssUpload(c *gin.Context){
	//讀取cookie
	cookie, _ := c.Cookie("go-cookie")
	log.Printf("go-cookie: ",cookie)

	//防止多次重複提交表單
	//解決方案是在表單中添加一個帶有唯一值的隱藏字段。
	// 在驗證表單時,先檢查帶有該唯一值的表單是否已經遞交過了。
	// 如果是,拒絕再次遞交;如果不是,則處理表單進行邏輯處理。
	res1 := verifyToken(c)
	if !res1 {
		return
	}

	//上傳文件
	header, err := c.FormFile("files")
	if err != nil {
		//ignore
		SendFailure(c,"上傳失敗")
		return
	}
	localfile := "./uploads/"+header.Filename //本地文件路徑
	// gin 簡單做了封裝,拷貝了文件流
	if err := c.SaveUploadedFile(header, localfile); err != nil {
		log.Printf("SaveUploadedFile err: ",err)
		// ignore
		SendFailure(c,"本地上傳失敗")
		return
	}
	log.Printf("本地上傳成功")

	//上傳到阿里雲oss
	dateyear := time.Now().Format("2006") //獲取當前年
	datemonth := time.Now().Format("01")//獲取當前月
	dateday := time.Now().Format("02")//獲取當前日
	yunfiletmp := fmt.Sprintf("uploads/%v/%v/%v/%v",dateyear,datemonth,dateday,header.Filename)
	yunfile,err := model.UploadFile(localfile,yunfiletmp)
	if err != nil{
		log.Printf("UploadFile err: ",err)
		// ignore
		SendFailure(c,"阿里雲上傳失敗")
		return
	}
	log.Printf("阿里雲路徑: ",yunfile)
	domain := viper.GetString("common.aliyunoss.domain")
	domain = fmt.Sprintf("%s/%s",domain,yunfile)
	oss := model.OssStruct{
		Url: domain,
	}
	log.Printf("阿里雲上傳成功: %s",oss)
	SendSuccess(c,"阿里雲路徑",oss)
}

//防止多次重複提交表單
func verifyToken(c *gin.Context) bool{
	token := c.PostForm("token")

	log.Printf("token: %s",token)
	if token != ""{
		// 驗證 token 的合法性
		if len(token) <10{
			SendFailure(c,"token驗證失敗")
			return false
		}
	}else{
		//不存在token 報錯
		SendFailure(c,"token驗證失敗")
		return false
	}
	log.Printf("token驗證通過")
	return true
}

建立文件夾 goossserver/uploads 權限爲777

建立文件夾和文件 goossserver/main.go ,main.go 內容如下:

package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"

	"goossserver/config"
	"goossserver/router"
	"goossserver/service"
)

func main() {
	if err := config.Init();err != nil{
		panic(err)
	}
	//設置gin模式
	gin.SetMode(viper.GetString("common.server.runmode"))

	//創建一個gin引擎
	g := gin.New()

	router.InitRouter(g)
	log.Printf("開始監聽服務器地址: %s\n", viper.GetString("common.server.url"))

	//使用熱重啓
	// kill -USR2 pid 重啓
	// kill -INT pid 關閉
	add := viper.GetString("common.server.addr")
	srv := &http.Server{
		Addr:    add,
		Handler: g,
	}

	log.Printf( "srv.Addr  %v  \n", srv.Addr)
	service.Listenserver(srv)

}

初始化模塊

[root@izj6c4jirdug8kh3uo6rdez goossserver]# go mod init goossserver
go: creating new go.mod: module goossserver

打包並運行服務

# 打包到本地
[root@izj6c4jirdug8kh3uo6rdez goossserver]# go build

# 運行
[root@izj6c4jirdug8kh3uo6rdez goossserver]# ./goossserver
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /ossupload                --> goossserver/service.OssUpload (5 handlers)
[root@izj6c4jirdug8kh3uo6rdez goossserver]# [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /ossupload                --> goossserver/service.OssUpload (5 handlers)

服務器端結束

 

參考walk:https://github.com/lxn/walk

參考:https://blog.csdn.net/kgjn__/article/details/89288550

參考walk使用:https://studygolang.com/articles/10755

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章