依赖包

差点烂尾。但多亏齐老师提醒,我才意识到已经超过一周没有写文章了。最近手边事情有些多,所以没有精力做到日更。但我会努力更新下去,事乃成功归于足下,就这么一篇一篇的更下去,总有会收获的那天。

这篇文章聊聊golang依赖包的事情。从前四式可以看出,golang程序中很多依赖包都需要从github下载,还有一部分依赖包需要从golang.org中下载。github还好办些,至少GFW还会分时分段放行。但golang.org就完全扯淡了,GFW不给你丁点放行的机会。当你通过go get golang.org/xxx时,死的心都有。当你改不了世界,就想法去苟且吧。所以这篇文章就来给你个苟且方案。

苟且方案一:

所有的golang.org代码都同步在github.com/golang中,所以当你需要golang.org/某些数据时,就先go get github.com/golang/xxx。然后自行rename。虽然多了一些手动的步骤,但至少能让你code下去,所以算作方案一。如果不满足,就继续看苟且方案二。

苟且方案二:

在终端环境设置代理,推荐的代理就是大名鼎鼎的shadowsocks。让go get的请求都通过代理去翻墙。如果想看具体怎么设置,首先要有一个shadowsocks环境,然后建议看看我的视频https://youtu.be/YDSQrjsOV7I ,这个视频介绍了如何通过provixy和shadowsocks来为终端翻墙。这个方案优点在于完全自动化,几乎可以忽略GFW的存在,同时也可以代理其它终端工具。但也有缺点,就是依赖网速,网速慢了也是扯淡。在这个方案基础之上,也有苟且方案三。

苟且方案三:

如果你的服务器需要翻墙,我把苟且方案二最核心的provixy和shadowsocks cli封装成了docker镜像,直接启动就能用。镜像名称是vikings/shadowsocks, 如果有不明白的地方,可以发邮件或者在https://github.com/andy-zhangtao/AwesomeDockerfile 提issue。好了,下面是用golang来解决golang的苟且方案四。

苟且方案四:

方案四本质也是代理,但不走shadowsocks,只是用golang来写一个small proxy,然后只提供依赖包的下载功能。首先,我们来看看这个方案是怎么实现的。下面是大致的实现流程

Leadfoot Server是一个运行在golang开发环境中的应用程序,当用户需要执行go get时,就把请求发给Leadfoot,当Leadfoot接受到请求后,就会代替用户执行go get。然后将所有接受到的代码打包成zip文件。最后将这个打包文件返回给用户端,用户端执行unzip就有所有需要的依赖代码了。

综上所述,Leadfoot肯定会有配合使用的Server和Client。所以先来看Server端的实现。

在开始讲解之前,先看一个结构体.

type Down struct {
    Path string `json:"path"`
    Md5  string
}

这个结构体用来标示唯一的用户请求。因为每个请求都是无状态并且异步完成的,为了不重复下载数据,需要标示每个请求。如何使用,后面会提到。

当用户需要下载依赖包时,会调用server的下载API,下面是此API的具体实现.

func Download(w http.ResponseWriter, r *http.Request) {
    data, err := ioutil.ReadAll(r.Body)
    if err != nil {
        Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    d := &Down{}

    err = json.Unmarshal(data, d)
    if err != nil {
        Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
        return
    }
    md := md5.Sum([]byte(d.Path))
    d.Md5 = hex.EncodeToString(md[:len(md)])

    STATUS[d.Md5] = DOWNING
    err = GoGet(d)
    if err != nil {
        STATUS[d.Md5] = ERROR
        // ioutil.WriteFile(TEMP+"/"+d.Md5+".err", []byte(err.Error()), 777)
        Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
        return
    }
    STATUS[d.Md5] = DOWNDONE
}

客户端通过POST方法,在body中写入需要下载的数据。目前需要的数据就是path一个字段。

当Leadfoot接收到数据之后,计算出path相对应的md5值,然后使用md5作为key来存储状态(后面所有针对这个path的查询都已md5为准),并且标示为正在下载。

GoGet就用来下载path,下面是其实现

func GoGet(d *Down) error {
    os.MkdirAll(TEMP+d.Md5+"/src", 0777)
    err := os.Setenv("GOPATH", TEMP+d.Md5)
    if err != nil {
        return err
    }
    cmd := exec.Command("go", "get", d.Path)
    // cmd.Env = append(cmd.Env, "$GOPATH=")
    var out bytes.Buffer
    cmd.Stdout = &out
    cmd.Stderr = &out
    err = cmd.Run()
    if err != nil {
        file, _ := os.Create(TEMP + "/" + d.Md5 + ".err")
        out.WriteTo(file)
        return err
    }

    // pack := strings.Split(d.Path, "/")[0]
    Zip(d.Md5, os.Getenv("GOPATH")+"/src/")
    return nil
}

目前来说,GoGet没有使用特殊的解决方案,就是简单的调用系统命令来执行go get。其中有一个小技巧,就是每次下载时都重新设定GOPATH,这是因为每个请求所下载的数据都不同。如果不区分出这些请求,那么后面打包时,所有数据就都混为一谈了。因此有必要设置出隔离的小环境,而设置隔离就是通过GOPATH。如果忘记了GOPATH的用法,返回到第一式去看看吧。

当数据下载完成之后,就自动进行打包,也就是Zip函数.

func Zip(md5, dir string) {
    tmpDir, err := ioutil.TempDir("/tmp", md5+"_zip_")
    if err != nil {
        panic(err)
    }
    defer func() {
        _ = os.RemoveAll("/tmp/" + md5)
    }()

    outFilePath := filepath.Join(tmpDir, md5+".zip")
    fmt.Println(outFilePath)
    progress := func(archivePath string) {
        fmt.Println(archivePath)
    }

    err = zip.ArchiveFile(dir, outFilePath, progress)
    if err != nil {
        panic(err)
    }

    DONE[md5] = outFilePath
}

zip文件也是以md5来标示,这是因为如果下次有请求时,会尝试看看有没有同名的zip文件,方便节省资源。好了,下载阶段就完成了。

客户端只有当server端下载完成之后,才能开始下载zip文件。因此server端就会提供一个查询接口。也就是下面的函数:

func Query(w http.ResponseWriter, r *http.Request) {
    pack := r.Header.Get("package")
    if pack == "" {
        Sandstorm.HTTPError(w, "I need a header named `package`", http.StatusInternalServerError)
        return
    }

    md := md5.Sum([]byte(pack))
    m := hex.EncodeToString(md[:len(md)])

    switch STATUS[m] {
    case ERROR:
        content, _ := ioutil.ReadFile(TEMP + "/" + m + ".err")
        Sandstorm.HTTPSuccess(w, "GO GET ERROR["+string(content)+"]")
    case DOWNING:
        Sandstorm.HTTPSuccess(w, "Package downloading... ")
    case DOWNDONE:
        Sandstorm.HTTPSuccess(w, CANDOWN)
    }

    return
}

这里面可以做个优化,客户端可以使用md5作为查询请求,但目前使用的是path。server会根据path再计算一次md5,所有有些浪费资源。

当客户端接收到DOWNDONE之后,就可以拉取zip文件了。下面是拉取的API:

func Pull(w http.ResponseWriter, r *http.Request) {
    //First of check if Get is set in the URL
    // Filename := request.URL.Query().Get("file")
    pack := r.Header.Get("package")
    if pack == "" {
        Sandstorm.HTTPError(w, "I need a header named `package`", http.StatusInternalServerError)
        return
    }

    md := md5.Sum([]byte(pack))
    Filename := DONE[hex.EncodeToString(md[:len(md)])]

    fmt.Println("Client requests: " + Filename)

    //Check if file exists and open
    Openfile, err := os.Open(Filename)
    defer Openfile.Close() //Close after function return
    if err != nil {
        //File not found, send 404
        http.Error(w, "File not found.", 404)
        return
    }

    //File is found, create and send the correct headers

    //Get the Content-Type of the file
    //Create a buffer to store the header of the file in
    FileHeader := make([]byte, 512)
    //Copy the headers into the FileHeader buffer
    Openfile.Read(FileHeader)
    //Get content type of file
    FileContentType := http.DetectContentType(FileHeader)

    //Get the file size
    FileStat, _ := Openfile.Stat()                     //Get info from file
    FileSize := strconv.FormatInt(FileStat.Size(), 10) //Get file size as a string

    //Send the headers
    w.Header().Set("Content-Disposition", "attachment; filename="+Filename)
    w.Header().Set("Content-Type", FileContentType)
    w.Header().Set("Content-Length", FileSize)

    //Send the file
    //We read 512 bytes from the file already so we reset the offset back to 0
    Openfile.Seek(0, 0)
    io.Copy(w, Openfile) //'Copy' the file to the client
    return
}

这里使用了Http的GET方法。而因为依赖包的名称和url很相似,所以就放在了Header当中。因此第一步就是从Header中取出package名称。后面就是设置Header值,然后读取本地文件数据再写入ResponseWriter中。

这些就是Server的处理逻辑,相对于Server而言,Client就显得很简单了。首先调用DownloadAPI,然后定时查询QueryAPI,最后调用Pull API,当接受完数据之后,执行Unzip就可以了。因为Server打包数据时,是按照golang规范目录结构打包的,所以直接将文件unzip到GOPATH当中,并且选择覆盖旧数据,就完成更新了。

所有的源代码都保存在https://github.com/andy-zhangtao/Leadfoot 可以去上面看看所有的源码。当前我提供了一个Leadfoot Server,如果你想试用一下,可以执行下面的命令来测试:

Leadfoot client -s http://leadfoot.openss.cc -p github.com/knq/chromedp

好了,Leadfoot就介绍到这里了。

results matching ""

    No results matching ""