mirror of https://github.com/mvdan/fdroidcl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
5.0 KiB
218 lines
5.0 KiB
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc> |
|
// See LICENSE for licensing information |
|
|
|
package main |
|
|
|
import ( |
|
"bytes" |
|
"crypto/sha256" |
|
"encoding/gob" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"os" |
|
"path/filepath" |
|
"runtime" |
|
"sort" |
|
"time" |
|
|
|
"github.com/schollz/progressbar/v3" |
|
"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, target_path string, sum []byte) error { |
|
req, err := http.NewRequest("GET", url, nil) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
etagPath := target_path + "-etag" |
|
if _, err := os.Stat(target_path); err == nil { |
|
etag, _ := os.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("%s download failed: %d %s", |
|
url, resp.StatusCode, http.StatusText(resp.StatusCode)) |
|
} |
|
if resp.StatusCode == http.StatusNotModified { |
|
fmt.Printf("%s not modified\n", url) |
|
return errNotModified |
|
} |
|
f, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) |
|
if err != nil { |
|
return err |
|
} |
|
defer f.Close() |
|
bar := progressbar.NewOptions64( |
|
resp.ContentLength, |
|
progressbar.OptionSetDescription(url), |
|
progressbar.OptionSetWriter(os.Stdout), |
|
progressbar.OptionShowBytes(true), |
|
progressbar.OptionThrottle(50*time.Millisecond), |
|
progressbar.OptionShowCount(), |
|
progressbar.OptionOnCompletion(func() { |
|
fmt.Fprint(os.Stdout, "\n") |
|
}), |
|
progressbar.OptionSpinnerType(14), |
|
progressbar.OptionSetRenderBlankState(true), |
|
progressbar.OptionUseANSICodes(runtime.GOOS != "windows"), |
|
progressbar.OptionFullWidth(), |
|
) |
|
if sum == nil { |
|
_, err := io.Copy(io.MultiWriter(f, bar), resp.Body) |
|
if err != nil { |
|
return err |
|
} |
|
} else { |
|
hash := sha256.New() |
|
_, err := io.Copy(io.MultiWriter(f, bar, hash), resp.Body) |
|
if err != nil { |
|
return err |
|
} |
|
got := hash.Sum(nil) |
|
if !bytes.Equal(sum, got[:]) { |
|
return fmt.Errorf("%s sha256 mismatch", url) |
|
} |
|
} |
|
if err := os.WriteFile(etagPath, []byte(respEtag(resp)), 0o644); err != nil { |
|
return err |
|
} |
|
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] |
|
app.FdroidRepoName = r.ID |
|
app.FdroidRepoURL = r.URL |
|
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 |
|
}
|
|
|