// Copyright (c) 2015, Daniel Martí // See LICENSE for licensing information package main import ( "bytes" "crypto/sha256" "encoding/gob" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "sort" "mvdan.cc/fdroidcl/fdroid" ) var cmdUpdate = &Command{ UsageLine: "update", Short: "Update the index", } func init() { cmdUpdate.Run = runUpdate } func runUpdate(args []string) error { anyModified := false for _, r := range config.Repos { if !r.Enabled { continue } if err := r.updateIndex(); err == errNotModified { } else if err != nil { return fmt.Errorf("could not update index: %v", err) } else { anyModified = true } } if anyModified { cachePath := filepath.Join(mustCache(), "cache-gob") os.Remove(cachePath) } return nil } const jarFile = "index-v1.jar" func (r *repo) updateIndex() error { url := fmt.Sprintf("%s/%s", r.URL, jarFile) return downloadEtag(url, indexPath(r.ID), nil) } func (r *repo) loadIndex() (*fdroid.Index, error) { p := indexPath(r.ID) f, err := os.Open(p) if os.IsNotExist(err) { return nil, fmt.Errorf("index does not exist; try 'fdroidcl update'") } else if err != nil { return nil, fmt.Errorf("could not open index: %v", err) } stat, err := f.Stat() if err != nil { return nil, fmt.Errorf("could not stat index: %v", err) } return fdroid.LoadIndexJar(f, stat.Size(), nil) } func respEtag(resp *http.Response) string { etags, e := resp.Header["Etag"] if !e || len(etags) == 0 { return "" } return etags[0] } var errNotModified = fmt.Errorf("not modified") var httpClient = &http.Client{} func downloadEtag(url, path string, sum []byte) error { fmt.Printf("Downloading %s... ", url) defer fmt.Println() req, err := http.NewRequest("GET", url, nil) if err != nil { return err } etagPath := path + "-etag" if _, err := os.Stat(path); err == nil { etag, _ := ioutil.ReadFile(etagPath) req.Header.Add("If-None-Match", string(etag)) } resp, err := httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("download failed: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) } if resp.StatusCode == http.StatusNotModified { fmt.Printf("not modified") return errNotModified } f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer f.Close() if sum == nil { _, err := io.Copy(f, resp.Body) if err != nil { return err } } else { data, err := ioutil.ReadAll(resp.Body) if err != nil { return err } got := sha256.Sum256(data) if !bytes.Equal(sum, got[:]) { return fmt.Errorf("sha256 mismatch") } if _, err := f.Write(data); err != nil { return err } } if err := ioutil.WriteFile(etagPath, []byte(respEtag(resp)), 0644); err != nil { return err } fmt.Printf("done") return nil } func indexPath(name string) string { return filepath.Join(mustData(), name+".jar") } const cacheVersion = 2 type cache struct { Version int Apps []fdroid.App } type apkPtrList []*fdroid.Apk func (al apkPtrList) Len() int { return len(al) } func (al apkPtrList) Swap(i, j int) { al[i], al[j] = al[j], al[i] } func (al apkPtrList) Less(i, j int) bool { return al[i].VersCode > al[j].VersCode } func loadIndexes() ([]fdroid.App, error) { cachePath := filepath.Join(mustCache(), "cache-gob") if f, err := os.Open(cachePath); err == nil { defer f.Close() var c cache if err := gob.NewDecoder(f).Decode(&c); err == nil && c.Version == cacheVersion { return c.Apps, nil } } m := make(map[string]*fdroid.App) for _, r := range config.Repos { if !r.Enabled { continue } index, err := r.loadIndex() if err != nil { return nil, fmt.Errorf("error while loading %s: %v", r.ID, err) } for i := range index.Apps { app := index.Apps[i] orig, e := m[app.PackageName] if !e { m[app.PackageName] = &app continue } apks := append(orig.Apks, app.Apks...) // We use a stable sort so that repository order // (priority) is preserved amongst apks with the same // vercode on apps sort.Stable(apkPtrList(apks)) m[app.PackageName].Apks = apks } } apps := make([]fdroid.App, 0, len(m)) for _, a := range m { apps = append(apps, *a) } sort.Sort(fdroid.AppList(apps)) if f, err := os.Create(cachePath); err == nil { defer f.Close() gob.NewEncoder(f).Encode(cache{ Version: cacheVersion, Apps: apps, }) } return apps, nil }