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.
253 lines
5.0 KiB
253 lines
5.0 KiB
/* Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc> */ |
|
/* See LICENSE for licensing information */ |
|
|
|
package fdroidcl |
|
|
|
import ( |
|
"archive/zip" |
|
"bytes" |
|
"encoding/xml" |
|
"errors" |
|
"fmt" |
|
"io" |
|
"io/ioutil" |
|
"net/http" |
|
"os" |
|
"strings" |
|
) |
|
|
|
// Repo is an F-Droid repository holding apps and apks |
|
type Repo struct { |
|
Apps []App `xml:"application"` |
|
} |
|
|
|
type CommaList []string |
|
|
|
func (cl *CommaList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { |
|
var content string |
|
if err := d.DecodeElement(&content, &start); err != nil { |
|
return err |
|
} |
|
*cl = strings.Split(content, ",") |
|
return nil |
|
} |
|
|
|
// App is an Android application |
|
type App struct { |
|
ID string `xml:"id"` |
|
Name string `xml:"name"` |
|
Summary string `xml:"summary"` |
|
Desc string `xml:"desc"` |
|
License string `xml:"license"` |
|
Categs CommaList `xml:"categories"` |
|
Website string `xml:"web"` |
|
Source string `xml:"source"` |
|
Tracker string `xml:"tracker"` |
|
Donate string `xml:"donate"` |
|
Bitcoin string `xml:"bitcoin"` |
|
Litecoin string `xml:"litecoin"` |
|
Dogecoin string `xml:"dogecoin"` |
|
FlattrID string `xml:"flattr"` |
|
Apks []Apk `xml:"package"` |
|
CVName string `xml:"marketversion"` |
|
CVCode uint `xml:"marketvercode"` |
|
CurApk *Apk |
|
} |
|
|
|
// Apk is an Android package |
|
type Apk struct { |
|
VName string `xml:"version"` |
|
VCode uint `xml:"versioncode"` |
|
Size int `xml:"size"` |
|
MinSdk int `xml:"sdkver"` |
|
MaxSdk int `xml:"maxsdkver"` |
|
ABIs CommaList `xml:"nativecode"` |
|
} |
|
|
|
func (app *App) calcCurApk() { |
|
for _, apk := range app.Apks { |
|
app.CurApk = &apk |
|
if app.CVCode >= apk.VCode { |
|
break |
|
} |
|
} |
|
} |
|
|
|
func (app *App) TextDesc(w io.Writer) { |
|
reader := strings.NewReader(app.Desc) |
|
decoder := xml.NewDecoder(reader) |
|
firstParagraph := true |
|
linePrefix := "" |
|
colsUsed := 0 |
|
var links []string |
|
linked := false |
|
for { |
|
token, err := decoder.Token() |
|
if err == io.EOF || token == nil { |
|
break |
|
} |
|
switch t := token.(type) { |
|
case xml.StartElement: |
|
switch t.Name.Local { |
|
case "p": |
|
if firstParagraph { |
|
firstParagraph = false |
|
} else { |
|
fmt.Fprintln(w) |
|
} |
|
linePrefix = "" |
|
colsUsed = 0 |
|
case "li": |
|
fmt.Fprint(w, "\n *") |
|
linePrefix = " " |
|
colsUsed = 0 |
|
case "a": |
|
for _, attr := range t.Attr { |
|
if attr.Name.Local == "href" { |
|
links = append(links, attr.Value) |
|
linked = true |
|
break |
|
} |
|
} |
|
} |
|
case xml.EndElement: |
|
switch t.Name.Local { |
|
case "p": |
|
fmt.Fprintln(w) |
|
case "ul": |
|
fmt.Fprintln(w) |
|
case "ol": |
|
fmt.Fprintln(w) |
|
} |
|
case xml.CharData: |
|
left := string(t) |
|
if linked { |
|
left += fmt.Sprintf("[%d]", len(links)-1) |
|
linked = false |
|
} |
|
limit := 80 - len(linePrefix) - colsUsed |
|
firstLine := true |
|
for len(left) > limit { |
|
last := 0 |
|
for i, c := range left { |
|
if i >= limit { |
|
break |
|
} |
|
if c == ' ' { |
|
last = i |
|
} |
|
} |
|
if firstLine { |
|
firstLine = false |
|
limit += colsUsed |
|
} else { |
|
fmt.Fprint(w, linePrefix) |
|
} |
|
fmt.Fprintln(w, left[:last]) |
|
left = left[last+1:] |
|
colsUsed = 0 |
|
} |
|
if firstLine { |
|
firstLine = false |
|
} else { |
|
fmt.Fprint(w, linePrefix) |
|
} |
|
fmt.Fprint(w, left) |
|
colsUsed += len(left) |
|
} |
|
} |
|
if len(links) > 0 { |
|
fmt.Fprintln(w) |
|
for i, link := range links { |
|
fmt.Fprintf(w, "[%d] %s\n", i, link) |
|
} |
|
} |
|
} |
|
|
|
func (app *App) prepareData() { |
|
app.calcCurApk() |
|
} |
|
|
|
var ErrNotModified = errors.New("etag matches, file was not modified") |
|
|
|
func downloadEtag(url, path string) error { |
|
client := &http.Client{} |
|
req, err := http.NewRequest("GET", url, nil) |
|
|
|
etagPath := path + "-etag" |
|
if _, err := os.Stat(path); err == nil { |
|
etag, _ := ioutil.ReadFile(etagPath) |
|
req.Header.Add("If-None-Match", string(etag)) |
|
} |
|
|
|
resp, err := client.Do(req) |
|
if err != nil { |
|
return err |
|
} |
|
defer resp.Body.Close() |
|
if resp.StatusCode == http.StatusNotModified { |
|
return ErrNotModified |
|
} |
|
jar, err := ioutil.ReadAll(resp.Body) |
|
if err != nil { |
|
return err |
|
} |
|
err = ioutil.WriteFile(path, jar, 0644) |
|
err2 := ioutil.WriteFile(etagPath, []byte(resp.Header["Etag"][0]), 0644) |
|
if err != nil { |
|
return err |
|
} |
|
if err2 != nil { |
|
return err2 |
|
} |
|
return nil |
|
} |
|
|
|
func indexPath(repoName string) string { |
|
return repoName + ".jar" |
|
} |
|
|
|
func UpdateIndex(repoName, repoURL string) error { |
|
path := indexPath(repoName) |
|
url := fmt.Sprintf("%s/%s", repoURL, path) |
|
if err := downloadEtag(url, path); err != nil { |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
func LoadRepo(repoName string) (*Repo, error) { |
|
path := indexPath(repoName) |
|
r, err := zip.OpenReader(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer r.Close() |
|
buf := new(bytes.Buffer) |
|
|
|
for _, f := range r.File { |
|
if f.Name != "index.xml" { |
|
continue |
|
} |
|
rc, err := f.Open() |
|
if err != nil { |
|
return nil, err |
|
} |
|
if _, err = io.Copy(buf, rc); err != nil { |
|
return nil, err |
|
} |
|
rc.Close() |
|
break |
|
} |
|
|
|
var repo Repo |
|
if err := xml.Unmarshal(buf.Bytes(), &repo); err != nil { |
|
return nil, err |
|
} |
|
|
|
for i := range repo.Apps { |
|
app := &repo.Apps[i] |
|
app.prepareData() |
|
} |
|
return &repo, nil |
|
}
|
|
|