1// Copyright (C) 2013-2015 Sören Tempel 2// 3// This program is free software: you can redistribute it and/or modify 4// it under the terms of the GNU General Public License as published by 5// the Free Software Foundation, either version 3 of the License, or 6// (at your option) any later version. 7// 8// This program is distributed in the hope that it will be useful, 9// but WITHOUT ANY WARRANTY; without even the implied warranty of 10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11// GNU General Public License for more details. 12// 13// You should have received a copy of the GNU General Public License 14// along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16package util 17 18import ( 19 "errors" 20 "fmt" 21 "io" 22 "net" 23 "net/http" 24 "net/url" 25 "os" 26 "path" 27 "path/filepath" 28 "strings" 29 "time" 30) 31 32const ( 33 // Number of times a failed HTTP request is retried. 34 retry = 3 35 36 // Number of maximal allowed redirects. 37 maxRedirects = 10 38 39 // HTTP User-Agent. 40 useragent = "cpod" 41) 42 43// Get performs a HTTP GET request, just like http.get, however, it has 44// a few handy extra features: I adds a User-Agent header and it retries 45// a failed get request if the error was a temporary one. 46func Get(uri string) (resp *http.Response, err error) { 47 req, err := http.NewRequest("GET", uri, nil) 48 if err != nil { 49 return 50 } 51 52 return doReq(req) 53} 54 55// GetFile downloads the file from the given uri and stores it in the 56// specified target directory. If a download was interrupted previously 57// GetFile is able to resume it. 58func GetFile(uri, target string) (fp string, err error) { 59 if err = os.MkdirAll(target, 0755); err != nil { 60 return 61 } 62 63 fn, err := filename(uri) 64 if err != nil { 65 return 66 } 67 68 fp = filepath.Join(target, fn) 69 partPath := fmt.Sprintf("%s.part", fp) 70 if _, err = os.Open(partPath); os.IsNotExist(err) { 71 if err = newGet(uri, partPath); err != nil { 72 return 73 } 74 } else { 75 if err = resumeGet(uri, partPath); err != nil { 76 return 77 } 78 } 79 80 if err = os.Rename(partPath, fp); err != nil { 81 return 82 } 83 84 return 85} 86 87// resumeGet resumes an canceled download started by the newGet 88// function. 89func resumeGet(uri, target string) error { 90 fi, err := os.Stat(target) 91 if err != nil { 92 return err 93 } 94 95 req, err := http.NewRequest("GET", uri, nil) 96 if err != nil { 97 return err 98 } 99100 req.Header.Add("Range", fmt.Sprintf("bytes=%d-", fi.Size()))101 req.Header.Add("If-Range", fi.ModTime().Format(time.RFC1123))102103 resp, err := doReq(req)104 if err != nil {105 return err106 }107108 defer resp.Body.Close()109 if resp.StatusCode != http.StatusPartialContent {110 return newGet(uri, target)111 }112113 file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644)114 if err != nil {115 return err116 }117118 defer file.Close()119 if _, err = io.Copy(file, resp.Body); err != nil {120 return err121 }122123 return nil124}125126// newGet starts a new file download, if the download wasn't completed127// it can be resumed later on using the resumeGet function.128func newGet(uri, target string) error {129 resp, err := Get(uri)130 if err != nil {131 return err132 }133134 reader := resp.Body135 defer reader.Close()136137 file, err := os.Create(target)138 if err != nil {139 return err140 }141142 defer file.Close()143 if _, err = io.Copy(file, reader); err != nil {144 return err145 }146147 return err148}149150// filename returns the fiilename of an URL. Basically it just uses151// path.Base to determine the filename but it also removes queries.152// Furthermore it also guarantees that the filename is not empty by153// setting it to "unnamed" if it couldn't determine a proper filename.154func filename(uri string) (fn string, err error) {155 u, err := url.Parse(uri)156 if err != nil {157 return158 }159160 fn = strings.TrimSpace(path.Base(u.Path))161 if len(fn) <= 0 || fn == "/" || fn == "." {162 fn = "unnamed"163 }164165 return166}167168// doReq does the same as net.client.Do but it retries sending the request if a169// temporary error on layer 4 is encountered. Furthermore, it also ensure that170// headers remain the same after a redirect and it adds a User-Agent header.171func doReq(req *http.Request) (resp *http.Response, err error) {172 req.Header.Add("User-Agent", useragent)173 client := headerClient(req.Header)174175 for i := 1; i <= retry; i++ {176 resp, err = client.Do(req)177 if nerr, ok := err.(net.Error); ok && (nerr.Temporary() || nerr.Timeout()) {178 time.Sleep(time.Duration(i*3) * time.Second)179 } else {180 break181 }182 }183184 return185}186187// headerClient returns a client witch a custom CheckRedirect function188// which ensures that the given headers will be readded after a redirect.189func headerClient(headers http.Header) *http.Client {190 redirectFunc := func(req *http.Request, via []*http.Request) error {191 if len(via) >= maxRedirects {192 return errors.New("too many redirects")193 }194195 req.Header = headers196 return nil197 }198199 return &http.Client{CheckRedirect: redirectFunc}200}