From 810a5677f255c267a91052a7a0c3d5e35c280056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Apr 2015 22:27:08 +0200 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ LICENSE | 27 +++++++++ fdroidcl.go | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 fdroidcl.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f527db --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Output binary +fdroidcl + +# Indexes +*.xml +*.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5babf24 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Daniel Martí. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/fdroidcl.go b/fdroidcl.go new file mode 100644 index 0000000..ea9f52f --- /dev/null +++ b/fdroidcl.go @@ -0,0 +1,157 @@ +/* Copyright (c) 2015, Daniel Martí */ +/* See LICENSE for licensing information */ + +package main + +import ( + "archive/zip" + "bytes" + "encoding/xml" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" +) + +type Repo struct { + Info RepoInfo `xml:"repo"` + Apps []App `xml:"application"` +} + +type RepoInfo struct{} + +type App struct { + Name string `xml:"name"` + ID string `xml:"id"` + Summary string `xml:"summary"` + License string `xml:"license"` + Categories string `xml:"categories"` + CVName string `xml:"marketversion"` + CVCode uint `xml:"marketvercode"` + Web string `xml:"web"` + Source string `xml:"source"` + Tracker string `xml:"tracker"` + Apks []Apk `xml:"package"` + CurApk *Apk +} + +type Apk struct { + VName string `xml:"version"` + VCode uint `xml:"versioncode"` + ApkName string `xml:"apkname"` + SrcName string `xml:"srcname"` +} + +func Form(f, str string) string { return fmt.Sprintf("\033[%sm%s\033[0m", f, str) } +func Bold(str string) string { return Form("1", str) } +func Green(str string) string { return Form("1;32", str) } +func Blue(str string) string { return Form("1;34", str) } +func Purple(str string) string { return Form("1;35", str) } + +func (app *App) Version() string { + if app.CurApk == nil { + for _, apk := range app.Apks { + app.CurApk = &apk + if app.CVCode >= apk.VCode { + break + } + } + } + return Green(app.CurApk.VName) +} + +func (app *App) WriteSummary(w io.Writer) { + fmt.Fprintf(w, "%s %s %s\n", Bold(app.Name), Purple(app.ID), app.Version()) + fmt.Fprintf(w, " %s\n", app.Summary) +} + +const indexName = "index.jar" + +var repoURL = flag.String("r", "https://f-droid.org/repo", "repository address") + +func updateIndex() { + url := fmt.Sprintf("%s/%s", *repoURL, indexName) + resp, err := http.Get(url) + if err != nil { + log.Fatalf("Failed to fetch '%s': %s", url, err) + } + defer resp.Body.Close() + out, err := os.Create(indexName) + if err != nil { + log.Fatalf("Failed to create file '%s': %s", indexName, err) + } + defer out.Close() + if _, err := io.Copy(out, resp.Body); err != nil { + log.Fatal(err) + } +} + +func loadApps() map[string]App { + r, err := zip.OpenReader(indexName) + if err != nil { + log.Fatal(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 { + log.Fatal(err) + } + if _, err = io.Copy(buf, rc); err != nil { + log.Fatal(err) + } + rc.Close() + break + } + + var repo Repo + if err := xml.Unmarshal(buf.Bytes(), &repo); err != nil { + log.Fatalf("Could not read xml: %s", err) + } + apps := make(map[string]App) + + for i := range repo.Apps { + app := repo.Apps[i] + apps[app.ID] = app + } + return apps +} + +func main() { + flag.Parse() + if flag.NArg() == 0 { + flag.Usage() + return + } + + cmd := flag.Args()[0] + args := flag.Args()[1:] + + switch cmd { + case "update": + updateIndex() + case "list": + apps := loadApps() + for _, app := range apps { + app.WriteSummary(os.Stdout) + } + case "show": + apps := loadApps() + for _, appID := range args { + app, e := apps[appID] + if !e { + log.Fatalf("Could not find app with ID '%s'", appID) + } + app.WriteSummary(os.Stdout) + } + default: + log.Fatalf("Unrecognised command '%s'", cmd) + } +}