diff --git a/asset.go b/asset.go new file mode 100644 index 0000000..8a7e8f7 --- /dev/null +++ b/asset.go @@ -0,0 +1,18 @@ +package pypihub + +import "fmt" + +type Asset struct { + ID int + Name string + Owner string + Repo string +} + +func (a Asset) String() string { + return a.Name +} + +func (a Asset) URL() string { + return fmt.Sprintf("/%s/%s/%d/%s", a.Owner, a.Repo, a.ID, a.Name) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..45fd47b --- /dev/null +++ b/client.go @@ -0,0 +1,100 @@ +package pypihub + +import ( + "io" + "net/http" + "strings" + + "github.com/google/go-github/github" +) + +type Client struct { + config Config + client *github.Client + repos []string +} + +func NewClient(cfg Config) *Client { + var t = github.BasicAuthTransport{ + Username: cfg.Username, + Password: cfg.AccessToken, + } + return &Client{ + config: cfg, + client: github.NewClient(t.Client()), + repos: cfg.RepoNames, + } +} + +func (c *Client) splitRepoName(r string) (string, string) { + var p = strings.SplitN(r, "/", 2) + switch len(p) { + case 2: + return p[0], p[1] + case 1: + return c.config.Username, p[0] + default: + return "", "" + } +} + +func (c *Client) GetRepoAssets(r string) ([]Asset, error) { + var owner, repo string + owner, repo = c.splitRepoName(r) + + var releases []*github.RepositoryRelease + var err error + releases, _, err = c.client.Repositories.ListReleases(owner, repo, nil) + if err != nil { + return nil, err + } + + var allAssets = make([]Asset, 0) + for _, rel := range releases { + var assets []*github.ReleaseAsset + assets, _, err = c.client.Repositories.ListReleaseAssets(owner, repo, *rel.ID, nil) + if err != nil { + return nil, err + } + for _, a := range assets { + allAssets = append(allAssets, Asset{ + ID: *a.ID, + Name: *a.Name, + Owner: owner, + Repo: repo, + }) + } + } + return allAssets, nil +} + +func (c *Client) GetAllAssets() ([]Asset, error) { + var allAssets = make([]Asset, 0) + for _, r := range c.config.RepoNames { + var repoAssets = make([]Asset, 0) + var err error + repoAssets, err = c.GetRepoAssets(r) + if err != nil { + return nil, err + } + allAssets = append(allAssets, repoAssets...) + } + + return allAssets, nil +} + +func (c *Client) DownloadAsset(a Asset) (io.ReadCloser, error) { + var rc io.ReadCloser + var redirect string + var err error + rc, redirect, err = c.client.Repositories.DownloadReleaseAsset(a.Owner, a.Repo, a.ID) + if rc == nil { + var resp *http.Response + resp, err = http.Get(redirect) + if err != nil { + return nil, err + } + rc = resp.Body + } + return rc, err +} diff --git a/cmd/pypihub/main.go b/cmd/pypihub/main.go index cee3f34..32651bd 100644 --- a/cmd/pypihub/main.go +++ b/cmd/pypihub/main.go @@ -10,4 +10,8 @@ func main() { var config pypihub.Config config = pypihub.ParseConfig() fmt.Printf("%#v\r\n", config) + + var server *pypihub.Server + server = pypihub.NewServer(config) + panic(server.ListenAndServe()) } diff --git a/config.go b/config.go index 4d4c54d..40238a4 100644 --- a/config.go +++ b/config.go @@ -26,7 +26,7 @@ func ParseConfig() Config { config.RepoNames = append(config.RepoNames, strings.Split(val, " ")...) } for i := range config.RepoNames { - config.RepoNames[i] = strings.Trim(config.RepoNames[i], " ") + config.RepoNames[i] = strings.TrimSpace(config.RepoNames[i]) } config.RepoNames = uniqueSlice(config.RepoNames) diff --git a/server.go b/server.go index 4b61034..55276bd 100644 --- a/server.go +++ b/server.go @@ -1 +1,162 @@ package pypihub + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +type Server struct { + client *Client + config Config + assets []Asset + timer *time.Timer +} + +func NewServer(cfg Config) *Server { + return &Server{ + client: NewClient(cfg), + config: cfg, + assets: make([]Asset, 0), + } +} + +func (s *Server) findAsset(owner string, repo string, id int) *Asset { + for _, a := range s.assets { + if a.Owner == owner && a.Repo == repo && a.ID == id { + return &a + } + } + return nil +} + +func (s *Server) refetchAssets() { + var err error + s.assets, err = s.client.GetAllAssets() + if err != nil { + fmt.Println(err) + } +} + +func (s *Server) startTimer() { + if s.timer != nil { + s.timer.Stop() + } + s.timer = time.AfterFunc(time.Duration(5*time.Minute), func() { + go s.refetchAssets() + }) +} + +func (s *Server) listAssets(w http.ResponseWriter, r *http.Request) { + var repo = strings.Trim(r.URL.Path, "/") + + w.Header().Add("Content-Type", "text/html") + fmt.Fprintf(w, "Links for %s", repo) + fmt.Fprintf(w, "

Links for %s

", repo) + for _, a := range s.assets { + if a.Repo == repo { + fmt.Fprintf(w, "%s ", a.URL(), a.Name) + } + } + fmt.Fprintf(w, "") +} + +func (s *Server) listAllAssets(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + fmt.Fprintf(w, "All asset links") + fmt.Fprintf(w, "

All asset links

") + for _, a := range s.assets { + fmt.Fprintf(w, "%s ", a.URL(), a.Name) + } + fmt.Fprintf(w, "") +} + +func (s *Server) listRepos(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + fmt.Fprintf(w, "Simple index") + for _, r := range s.config.RepoNames { + var parts = strings.SplitN(r, "/", 2) + fmt.Fprintf(w, "%s ", parts[1], parts[1]) + } + fmt.Fprintf(w, "") +} + +func (s *Server) fetchAsset(w http.ResponseWriter, r *http.Request) { + var url = strings.Trim(r.URL.Path, "/") + var parts = strings.SplitN(url, "/", 4) + + if len(parts) != 4 { + w.WriteHeader(http.StatusNotFound) + return + } + + var asset *Asset + var id int64 + var err error + id, err = strconv.ParseInt(parts[2], 10, 32) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + asset = s.findAsset(parts[0], parts[1], int(id)) + if asset == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + var rc io.ReadCloser + rc, err = s.client.DownloadAsset(*asset) + if err != nil || rc == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + defer rc.Close() + io.Copy(w, rc) +} + +func (s *Server) serveFavicon(w http.ResponseWriter, r *http.Request) { + var decoded []byte + var err error + decoded, err = base64.StdEncoding.DecodeString("AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAPs7/AJ9vNwBO3f8APMz/AEzb/wCdbjYAStn/AJZqNgBI1/8ARtX/AETT/wBA0f8AqnY3AD7P/wCjcjcApHI3ADzN/wBQ3v8AOsv/AE7c/wA4yf8Amm02ALF6OACYazYARtb/AETU/wCveTcAQtL/AKh1NwCtdzcAQND/AKFxNwCmczcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcDA4RExUAAAAAAAAAAAAZGhwfDhEAFQAAAAAAAAAACRkaHB8OABMAAAAAAAAAAAcJGRocHw4RAAAAAAAgAgYFBwkZGhwfARETFQAhDyACAwUHCQoaHB8BERMVHSEPIBIUBQcJChocHwEREw0dIQ8AEhQFBwkKGhwfAQQeDR0hDyACBhYYCAALHB8BGx4NHSEPIAIGFhgICgscHxcbHg0dIRAgAgYWGAkKCxwAFxseDR0hECACBhYHCQoAAAAAAB4NHSEQIAIGAAAAAAAAAAAbAA0dIRAgAgAAAAAAAAAAFwAeDR0hECAAAAAAAAAAAAAXGx4NHSEAAAAAAPgfAADwLwAA8C8AAPAPAACAAQAAAAAAAAAAAAAIAAAAABAAAAAAAAAAAAAAgAEAAPAPAAD0DwAA9A8AAPgfAAA=") + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + fmt.Fprintf(w, "%s", decoded) +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var parts = strings.Split(strings.Trim(r.URL.Path, "/"), "/") + parts = removeEmpty(parts) + + fmt.Println(r.URL.Path) + + switch len(parts) { + case 0: + s.listAllAssets(w, r) + case 1: + if parts[0] == "simple" { + s.listRepos(w, r) + } else if parts[0] == "favicon.ico" { + s.serveFavicon(w, r) + } else { + s.listAssets(w, r) + } + default: + s.fetchAsset(w, r) + } +} + +func (s *Server) ListenAndServe() error { + s.refetchAssets() + s.startTimer() + + http.Handle("/", s) + fmt.Println("Server listening at", s.config.Bind) + return http.ListenAndServe(s.config.Bind, nil) +} diff --git a/utils.go b/utils.go index 6036711..4bea6cc 100644 --- a/utils.go +++ b/utils.go @@ -12,3 +12,13 @@ func uniqueSlice(s []string) []string { } return o } + +func removeEmpty(s []string) []string { + var n = make([]string, 0) + for _, c := range s { + if c != "" { + n = append(n, c) + } + } + return n +}