F-Droid desktop client
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

// 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
}