1// Copyright (C) 2013-2015 Sören Tempel2//3// This program is free software: you can redistribute it and/or modify4// it under the terms of the GNU General Public License as published by5// the Free Software Foundation, either version 3 of the License, or6// (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 of10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11// GNU General Public License for more details.12//13// You should have received a copy of the GNU General Public License14// along with this program. If not, see <http://www.gnu.org/licenses/>.1516package util1718import (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)3132const (33 // Number of times a failed HTTP request is retried.34 retry = 33536 // Number of maximal allowed redirects.37 maxRedirects = 103839 // HTTP User-Agent.40 useragent = "cpod"41)4243// Get performs a HTTP GET request, just like http.get, however, it has44// a few handy extra features: I adds a User-Agent header and it retries45// 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 return50 }5152 return doReq(req)53}5455// GetFile downloads the file from the given uri and stores it in the56// specified target directory. If a download was interrupted previously57// 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 return61 }6263 fn, err := filename(uri)64 if err != nil {65 return66 }6768 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 return73 }74 } else {75 if err = resumeGet(uri, partPath); err != nil {76 return77 }78 }7980 if err = os.Rename(partPath, fp); err != nil {81 return82 }8384 return85}8687// resumeGet resumes an canceled download started by the newGet88// function.89func resumeGet(uri, target string) error {90 fi, err := os.Stat(target)91 if err != nil {92 return err93 }9495 req, err := http.NewRequest("GET", uri, nil)96 if err != nil {97 return err98 }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}