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