├── .gitignore ├── LICENSE ├── README.md └── src ├── build.sh ├── cross_build.sh ├── main.go └── qfetch ├── bucket.go └── fetch.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jemy Graw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/qiniu/qfetch)](https://goreportcard.com/report/github.com/qiniu/qfetch) 2 | 3 | # qfetch 4 | 5 | ### 简介 6 | qfetch是一个数据迁移工具,利用七牛提供的[fetch](http://developer.qiniu.com/docs/v6/api/reference/rs/fetch.html)功能来抓取指定文件列表中的文件。在文件列表中,你只需要提供资源的外链地址和要保存在七牛空间中的文件名就可以了。 7 | 8 | 使用该工具进行资源抓取的时候,可以根据需要随时可以中断任务的执行,下次重新使用原命令执行的时候,会自动跳过已经抓取成功的资源。 9 | 10 | ### 适用场景 11 | 12 | 该工具适用的场景需要满足如下条件: 13 | 14 | 1. 资源所在的源站必须具有较大的可用带宽,这样可以在业务低峰期进行资源抓取 15 | 2. 根据源站可用带宽和文件的平均大小,以及能够进行抓取的时间段,计算出数据迁移所需要的时间是否满足需求 16 | 17 | ### 下载 18 | 19 | **建议下载最新版本** 20 | 21 | |版本 |支持平台|链接| 22 | |--------|---------|----| 23 | |qfetch v1.8|Linux, Windows, Mac OSX|[下载](http://devtools.qiniu.com/qfetch-v1.8.zip)| 24 | 25 | ### 安装 26 | 27 | 该工具不需要安装,只需要从上面的下载链接下载zip包之后解压即可使用。其中文件名和对应系统关系如下: 28 | 29 | |文件名|描述| 30 | |-----|-----| 31 | |qfetch_linux_386|Linux 32位系统| 32 | |qfetch_linux_amd64|Linux 64位系统| 33 | |qfetch_linux_arm|Linux ARM CPU| 34 | |qfetch_windows_386.exe|Windows 32位系统| 35 | |qfetch_windows_amd64.exe|Windows 64位系统| 36 | |qfetch_darwin_386|Mac 32位系统,这种系统很老了| 37 | |qfetch_darwin_amd64|Mac 64位系统,主流的系统| 38 | 39 | 注意,如果在Linux或者Mac系统上遇到`Permission Denied`的错误,请使用命令`chmod +x qfetch`来为文件添加可执行权限。这里的`qfetch`是上面文件重命名之后的简写。 40 | 41 | 对于Linux或者Mac,如果希望能够在任何位置都可以执行,那么可以把`qfetch`所在的目录加入到环境变量`$PATH`中去。或者最简单的方法如下: 42 | 43 | ``` 44 | sudo mv qfetch /usr/local/bin 45 | ``` 46 | 另外,由于本工具是一个命令行工具,在Windows下面请先打开命令行终端,然后输入工具名称执行,不要双击打开。如果你希望可以在任意目录下使用qfetch,请将qfetch工具可执行文件所在目录添加到系统的环境变量中。 47 | 48 | ### 使用 49 | 该工具是一个命令行工具,需要指定相关的参数来运行。 50 | 51 | ``` 52 | Usage of qfetch: 53 | -ak="": qiniu access key 54 | -sk="": qiniu secret key 55 | -bucket="": qiniu bucket 56 | -job="": job name to record the progress 57 | -file="": resource list file to fetch 58 | -worker=1: max goroutine in a worker group 59 | -check-exists: check whether file exists in bucket 60 | -log="": save fetch runtime log to specified file 61 | -rs-host="": rs host to support specified qos system 62 | -io-host="": io host to support specified qos sytem 63 | ``` 64 | 65 | 66 | |命令|描述| 必须指定 | 67 | |--------|---------|-----------| 68 | |ak|七牛账号的AccessKey,可以从七牛的后台获取|是| 69 | |sk|七牛账号的SecretKey,可以从七牛的后台获取|是| 70 | |bucket|文件抓取后存储的空间,为空间的名字|是| 71 | |job|任务的名称,指定这个参数主要用来将抓取成功的文件放在本地数据库中,便于后面核对|是| 72 | |file|待抓取资源链接所在文件的本地路径,内容由待抓取的资源外链和对应的保存在七牛空间中的文件名组成的行构成|是| 73 | |worker|抓取的并发数量,可以适当地指定较大的并发请求数量来提高批量抓取的效率,可根据目标源站实际带宽和文件平均大小来计算得出|是| 74 | |check-exists|在抓取之前检查空间是否存在同名文件,如果存在则跳过,不抓取,当指定这个参数的时候为true,不指定为false|否| 75 | |log|抓取过程中打印的一些日志信息输出文件,如果不指定,则输出到终端|否| 76 | 77 | **多机房支持** 78 | 79 | qfetch目前已自动支持七牛的多机房。 80 | 81 | **模式一:** 82 | 83 | 上面的`file`参数指定的待抓取资源链接所在文件的行格式如下: 84 | 85 | ``` 86 | 文件链接1\t保存名称1 87 | 文件链接2\t保存名称2 88 | 文件链接3\t保存名称3 89 | ... 90 | ``` 91 | 92 | 其中`\t`表示Tab分隔符号。 93 | 94 | 例如: 95 | 96 | ``` 97 | http://img.abc.com/0/000/484/0000484193.fid 2009-10-14/2922168_b.jpg 98 | http://img.abc.com/0/000/553/0000553777.fid 2009-07-01/2270194_b.jpg 99 | http://img.abc.com/0/000/563/0000563511.fid 2009-03-01/1650739_s.jpg 100 | http://img.abc.com/0/000/563/0000563514.fid 2009-05-01/1953696_m.jpg 101 | http://img.abc.com/0/000/563/0000563515.fid 2009-02-01/1516376_s.jpg 102 | ``` 103 | 104 | 上面的方式最终抓取保存在空间中的文件名字是: 105 | 106 | ``` 107 | 2009-10-14/2922168_b.jpg 108 | 2009-07-01/2270194_b.jpg 109 | 2009-03-01/1650739_s.jpg 110 | 2009-05-01/1953696_m.jpg 111 | 2009-02-01/1516376_s.jpg 112 | ``` 113 | 114 | **模式二:** 115 | 116 | 上面的`file`参数指定的待抓取资源链接所在文件的行格式如下: 117 | 118 | ``` 119 | 文件链接1 120 | 文件链接2 121 | 文件链接3 122 | ... 123 | ``` 124 | 125 | 上面的方式也是支持的,这种方式的情况下,文件保存的名字将从指定的文件链接里面自动解析。 126 | 127 | 例如: 128 | 129 | ``` 130 | http://img.abc.com/0/000/484/0000484193.fid 131 | http://img.abc.com/0/000/553/0000553777.fid 132 | http://img.abc.com/0/000/563/0000563511.fid 133 | http://img.abc.com/0/000/563/0000563514.fid 134 | http://img.abc.com/0/000/563/0000563515.fid 135 | ``` 136 | 137 | 其抓取后保存在空间中的文件名字是: 138 | 139 | ``` 140 | 0/000/484/0000484193.fid 141 | 0/000/553/0000553777.fid 142 | 0/000/563/0000563511.fid 143 | 0/000/563/0000563514.fid 144 | 0/000/563/0000563515.fid 145 | ``` 146 | 147 | 148 | ### 日志 149 | 抓取成功的文件在本地都会写入以`job`参数指定的值为名称的本地leveldb数据库中。该数据库名称格式为`.job`,由于该leveldb名称以`.`开头,所以在Linux或者Mac系统下面是个隐藏文件。在整个文件索引都抓取完成后,可以使用[leveldb](https://github.com/jemygraw/leveldb)工具来导出所有的成功的文件列表,和原来的列表比较,就可以得出失败的抓取列表。上面的方法也可以被用来验证抓取的完整性。 150 | 151 | 另外抓取过程中发现回复为404的列表,单独放到`..404.job`的leveldb数据库中,抓取结束之后可以导出这部分数据做检查。 152 | 153 | 其中``就是参数`-job`所指定的名字,左右两边的`<`和`>`只是表示这个是参数,实际不存在。 154 | 155 | ### 示例 156 | 抓取指令为: 157 | 158 | ``` 159 | qfetch -ak 'x98pdzDw8dtwM-XnjCwlatqwjAeed3lwyjcNYqjv' -sk 'OCCTbp-zhD8x_spN0tFx4WnMABHxggvveg9l9m07' -bucket 'image' -file 'diff.txt' -job 'diff' -worker 100 -log 'run.log' -check-exists 160 | ``` 161 | **注意:** Windows系统下面使用该工具时,指定的参数两边不需要单引号。 162 | 163 | 上面的指令抓取文件索引`diff.txt`里面的文件,存储到空间`piccenter`里面,并发请求数量`300`,任务的名称叫做`diff`,成功列表日志文件名称是`.diff.job`。另外由于该命令打印的报警日志输出到终端,所以可以使用`tee`命令将内容复制一份到日志文件中。 164 | 165 | 导出成功列表: 166 | 167 | ``` 168 | leveldb -export '.diff.job' >> success.list.txt 169 | ``` 170 | 171 | 导出404列表: 172 | 173 | ``` 174 | leveldb -export '.diff.404.job' >> 404.list.txt 175 | ``` 176 | 177 | 注意,上面任务的名字是`diff`,而任务对应的的leveldb的名字是`.diff.job`。 178 | 179 | **经验** 180 | 181 | 一般来讲,如果是抓取任务的话,不一定需要导出最终成功的列表,只需要检查原始列表文件行数和抓取成功的文件行数一致就可以了。 182 | 183 | 使用下面方式获取列表行数: 184 | ``` 185 | $ wc -l diff.txt 186 | ``` 187 | 188 | 使用下面方式获取成功抓取数量: 189 | ``` 190 | $ leveldb -count '.diff.job' 191 | ``` 192 | 193 | 然后比较一致即可,如果发现数量不一致,可以重新运行原始命令(设置太大并发的情况下,存在失败的可能性)。 194 | 只要最后的结果没有错误或者都是404的错误,那么就是抓取成功了。404的错误可以后面跟进解决。 195 | 196 | 197 | ### 帮助 198 | 如果您遇到任何问题,可以加QQ群:343822521,我将乐意帮助您,非技术问题勿扰。 199 | -------------------------------------------------------------------------------- /src/build.sh: -------------------------------------------------------------------------------- 1 | DIR=$(cd ../; pwd) 2 | export GOPATH=$DIR:$GOPATH 3 | go build main.go 4 | -------------------------------------------------------------------------------- /src/cross_build.sh: -------------------------------------------------------------------------------- 1 | DIR=$(cd ../; pwd) 2 | export GOPATH=$DIR:$GOPATH 3 | GOOS="darwin" GOARCH="amd64" go build -o "../bin/qfetch_darwin_amd64" main.go 4 | GOOS="darwin" GOARCH="386" go build -o "../bin/qfetch_darwin_386" main.go 5 | GOOS="windows" GOARCH="amd64" go build -o "../bin/qfetch_windows_amd64.exe" main.go 6 | GOOS="windows" GOARCH="386" go build -o "../bin/qfetch_windows_386.exe" main.go 7 | GOOS="linux" GOARCH="amd64" go build -o "../bin/qfetch_linux_amd64" main.go 8 | GOOS="linux" GOARCH="386" go build -o "../bin/qfetch_linux_386" main.go 9 | GOOS="linux" GOARCH="arm" go build -o "../bin/qfetch_linux_arm" main.go 10 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "qfetch" 8 | "runtime" 9 | 10 | "github.com/qiniu/api.v6/auth/digest" 11 | "github.com/qiniu/api.v6/conf" 12 | ) 13 | 14 | func main() { 15 | runtime.GOMAXPROCS(runtime.NumCPU()) 16 | 17 | var job string 18 | var worker int 19 | var file string 20 | var bucket string 21 | var accessKey string 22 | var secretKey string 23 | var logFile string 24 | var checkExists bool 25 | 26 | //support qos, should be specified both 27 | var rsHost string 28 | var ioHost string 29 | 30 | flag.Usage = func() { 31 | fmt.Println(`Usage of qfetch: 32 | -ak="": qiniu access key 33 | -sk="": qiniu secret key 34 | -bucket="": qiniu bucket 35 | -job="": job name to record the progress 36 | -file="": resource list file to fetch 37 | -worker=1: max goroutine in a worker group 38 | -check-exists: check whether file exists in bucket 39 | -log="": save fetch runtime log to specified file 40 | -rs-host="": rs host to support specified qos system 41 | -io-host="": io host to support specified qos sytem 42 | version 1.8`) 43 | } 44 | 45 | flag.StringVar(&job, "job", "", "job name to record the progress") 46 | flag.IntVar(&worker, "worker", 1, "max goroutine in a worker group") 47 | flag.StringVar(&file, "file", "", "resource file to fetch") 48 | flag.StringVar(&bucket, "bucket", "", "qiniu bucket") 49 | flag.StringVar(&accessKey, "ak", "", "qiniu access key") 50 | flag.StringVar(&secretKey, "sk", "", "qiniu secret key") 51 | flag.StringVar(&logFile, "log", "", "fetch runtime log file") 52 | flag.BoolVar(&checkExists, "check-exists", false, "check whether file exists in bucket") 53 | flag.StringVar(&rsHost, "rs-host", "", "rs host to support specified qos system") 54 | flag.StringVar(&ioHost, "io-host", "", "io host to support specified qos system") 55 | 56 | flag.Parse() 57 | 58 | if accessKey == "" { 59 | fmt.Println("Error: accessKey is not set") 60 | return 61 | } 62 | 63 | if secretKey == "" { 64 | fmt.Println("Error: secretKey is not set") 65 | return 66 | } 67 | 68 | if bucket == "" { 69 | fmt.Println("Error: bucket is not set") 70 | return 71 | } 72 | 73 | if job == "" { 74 | fmt.Println("Error: job name is not set") 75 | return 76 | } 77 | 78 | if file == "" { 79 | fmt.Println("Error: resource file to fetch not set") 80 | return 81 | } 82 | _, ferr := os.Stat(file) 83 | if ferr != nil { 84 | fmt.Println(fmt.Sprintf("Error: file '%s' not exist", file)) 85 | return 86 | } 87 | 88 | if worker <= 0 { 89 | fmt.Println("Error: worker count must larger than zero") 90 | return 91 | } 92 | 93 | if (rsHost != "" && ioHost == "") || (rsHost == "" && ioHost != "") { 94 | fmt.Println("Error: rs host and io host should be specified together") 95 | return 96 | } 97 | 98 | mac := digest.Mac{ 99 | accessKey, []byte(secretKey), 100 | } 101 | 102 | if rsHost != "" && ioHost != "" { 103 | conf.IO_HOST = ioHost 104 | conf.RS_HOST = rsHost 105 | } else { 106 | //get bucket info 107 | bucktInfo, gErr := qfetch.GetBucketInfo(&mac, bucket) 108 | if gErr != nil { 109 | fmt.Println("Error: get bucket info error", gErr) 110 | return 111 | } else { 112 | switch bucktInfo.Region { 113 | case "z0": 114 | conf.RS_HOST = "http://rs.qbox.me" 115 | conf.IO_HOST = "http://iovip.qbox.me" 116 | case "z1": 117 | conf.RS_HOST = "http://rs-z1.qbox.me" 118 | conf.IO_HOST = "http://iovip-z1.qbox.me" 119 | case "z2": 120 | conf.RS_HOST = "http://rs-z2.qbox.me" 121 | conf.IO_HOST = "http://iovip-z2.qbox.me" 122 | case "na0": 123 | conf.RS_HOST = "http://rs-na0.qbox.me" 124 | conf.IO_HOST = "http://iovip-na0.qbox.me" 125 | case "as0": 126 | conf.RS_HOST = "http://rs-as0.qbox.me" 127 | conf.IO_HOST = "http://iovip-as0.qbox.me" 128 | } 129 | } 130 | } 131 | 132 | qfetch.Fetch(&mac, job, checkExists, file, bucket, logFile, worker) 133 | } 134 | -------------------------------------------------------------------------------- /src/qfetch/bucket.go: -------------------------------------------------------------------------------- 1 | package qfetch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/qiniu/api.v6/auth/digest" 7 | "github.com/qiniu/api.v6/rs" 8 | "github.com/qiniu/rpc" 9 | ) 10 | 11 | type BucketInfo struct { 12 | Region string `json:"region"` 13 | } 14 | 15 | var ( 16 | BUCKET_RS_HOST = "http://rs.qiniu.com" 17 | ) 18 | 19 | /* 20 | get bucket info 21 | @param mac 22 | @param bucket - bucket name 23 | @return bucketInfo, err 24 | */ 25 | func GetBucketInfo(mac *digest.Mac, bucket string) (bucketInfo BucketInfo, err error) { 26 | client := rs.New(mac) 27 | bucketUri := fmt.Sprintf("%s/bucket/%s", BUCKET_RS_HOST, bucket) 28 | callErr := client.Conn.Call(nil, &bucketInfo, bucketUri) 29 | if callErr != nil { 30 | if v, ok := callErr.(*rpc.ErrorInfo); ok { 31 | err = fmt.Errorf("code: %d, %s, xreqid: %s", v.Code, v.Err, v.Reqid) 32 | } else { 33 | err = callErr 34 | } 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /src/qfetch/fetch.go: -------------------------------------------------------------------------------- 1 | package qfetch 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/qiniu/api.v6/auth/digest" 13 | "github.com/qiniu/api.v6/rs" 14 | "github.com/qiniu/rpc" 15 | "github.com/syndtr/goleveldb/leveldb" 16 | "github.com/syndtr/goleveldb/leveldb/opt" 17 | ) 18 | 19 | var once sync.Once 20 | var fetchTasks chan func() 21 | 22 | func doFetch(tasks chan func()) { 23 | for { 24 | task := <-tasks 25 | task() 26 | } 27 | } 28 | 29 | func Fetch(mac *digest.Mac, job string, checkExists bool, fileListPath, bucket string, logFile string, worker int) { 30 | //open file list to fetch 31 | fh, openErr := os.Open(fileListPath) 32 | if openErr != nil { 33 | fmt.Println("Open resource file error,", openErr) 34 | return 35 | } 36 | defer fh.Close() 37 | 38 | //try open log file 39 | if logFile != "" { 40 | logFh, openErr := os.Create(logFile) 41 | if openErr != nil { 42 | log.SetOutput(os.Stdout) 43 | } else { 44 | log.SetOutput(logFh) 45 | defer logFh.Close() 46 | } 47 | } else { 48 | log.SetOutput(os.Stdout) 49 | defer os.Stdout.Sync() 50 | } 51 | 52 | ldbWOpt := opt.WriteOptions{ 53 | Sync: true, 54 | } 55 | 56 | //open leveldb success and not found 57 | successLdbPath := fmt.Sprintf(".%s.job", job) 58 | notFoundLdbPath := fmt.Sprintf(".%s.404.job", job) 59 | 60 | successLdb, lerr := leveldb.OpenFile(successLdbPath, nil) 61 | if lerr != nil { 62 | fmt.Println("Open fetch progress file error,", lerr) 63 | return 64 | } 65 | defer successLdb.Close() 66 | 67 | notFoundLdb, lerr := leveldb.OpenFile(notFoundLdbPath, nil) 68 | if lerr != nil { 69 | fmt.Println("Open fetch not found file error,", lerr) 70 | return 71 | } 72 | defer notFoundLdb.Close() 73 | 74 | client := rs.New(mac) 75 | //init work group 76 | once.Do(func() { 77 | fetchTasks = make(chan func(), worker) 78 | for i := 0; i < worker; i++ { 79 | go doFetch(fetchTasks) 80 | } 81 | }) 82 | 83 | fetchWaitGroup := sync.WaitGroup{} 84 | 85 | //scan each line and add task 86 | bReader := bufio.NewScanner(fh) 87 | bReader.Split(bufio.ScanLines) 88 | for bReader.Scan() { 89 | line := strings.TrimSpace(bReader.Text()) 90 | if line == "" { 91 | continue 92 | } 93 | 94 | items := strings.Split(line, "\t") 95 | if !(len(items) == 1 || len(items) == 2) { 96 | log.Printf("Invalid resource line `%s`\n", line) 97 | continue 98 | } 99 | 100 | resUrl := items[0] 101 | resKey := "" 102 | 103 | if len(items) == 1 { 104 | resUri, pErr := url.Parse(resUrl) 105 | if pErr != nil { 106 | log.Printf("Invalid resource url `%s`\n", resUrl) 107 | continue 108 | } 109 | resKey = resUri.Path 110 | if strings.HasPrefix(resKey, "/") { 111 | resKey = resKey[1:] 112 | } 113 | } else if len(items) == 2 { 114 | resKey = items[1] 115 | } 116 | 117 | //check from leveldb success whether it is done 118 | val, exists := successLdb.Get([]byte(resUrl), nil) 119 | if exists == nil && string(val) == resKey { 120 | log.Printf("Skip url fetched `%s` => `%s`\n", resUrl, resKey) 121 | continue 122 | } 123 | 124 | //check from leveldb not found whether it meet 404 125 | nfVal, nfExists := notFoundLdb.Get([]byte(resUrl), nil) 126 | if nfExists == nil && string(nfVal) == resKey { 127 | log.Printf("Skip url 404 `%s` => `%s`\n", resUrl, resKey) 128 | continue 129 | } 130 | 131 | //check whether file already exists in bucket 132 | if checkExists { 133 | if entry, err := client.Stat(nil, bucket, resKey); err == nil && entry.Hash != "" { 134 | successLdb.Put([]byte(resUrl), []byte(resKey), &ldbWOpt) 135 | log.Printf("Skip url exists `%s` => `%s`\n", resUrl, resKey) 136 | continue 137 | } 138 | } 139 | 140 | //otherwise fetch it 141 | fetchWaitGroup.Add(1) 142 | fetchTasks <- func() { 143 | defer fetchWaitGroup.Done() 144 | 145 | _, fErr := client.Fetch(nil, bucket, resKey, resUrl) 146 | if fErr == nil { 147 | successLdb.Put([]byte(resUrl), []byte(resKey), nil) 148 | } else { 149 | if v, ok := fErr.(*rpc.ErrorInfo); ok { 150 | if v.Code == 404 { 151 | notFoundLdb.Put([]byte(resUrl), []byte(resKey), &ldbWOpt) 152 | } 153 | log.Printf("Fetch `%s` error due to `%s`\n", resUrl, v.Err) 154 | } else { 155 | log.Printf("Fetch `%s` error due to `%s`\n", resUrl, fErr) 156 | } 157 | } 158 | } 159 | } 160 | 161 | //wait for all the fetch done 162 | fetchWaitGroup.Wait() 163 | } 164 | --------------------------------------------------------------------------------