diff --git a/main.go b/main.go index f6001029682b5d9eeb43060492c7410ea05110a2..9b9e61f805d9f33369b487b73cb104c1baecf168 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ func main() { http.HandleFunc("/api/releases.json", s.handleReleases) http.HandleFunc("/api/release/", s.handleReleaseMirror) http.HandleFunc("/api/apps/zip/", s.handleAppZip) + http.HandleFunc("/api/apps/tar/", s.handleAppTargz) log.Printf("Listening on %s...", flagListen) http.ListenAndServe(flagListen, nil) } diff --git a/server.go b/server.go index 6eb3daca0f6baa296443ab01a4677c38b32e4808..85cfd4931d1bce090ab3afbab38d79eef9715eb0 100644 --- a/server.go +++ b/server.go @@ -35,7 +35,7 @@ func (s *server) run(ctx context.Context) { log.Fatalf("Initial app fetch failed: %v", err) } - t := time.NewTicker(60 * time.Second) + t := time.NewTicker(5 * 60 * time.Second) for { select { case r := <-s.reqC: diff --git a/server_apps.go b/server_apps.go index d39377d5cedaa34b85ab99fca070739cd0727df8..b05800a6d4913971853e1be8651d1a6e74882649 100644 --- a/server_apps.go +++ b/server_apps.go @@ -1,8 +1,10 @@ package main import ( + "archive/tar" "archive/zip" "bytes" + "compress/gzip" "context" "encoding/json" "fmt" @@ -40,6 +42,7 @@ type appInfo struct { commitObj *object.Commit zip []byte + targz []byte } type GLProject struct { @@ -128,6 +131,65 @@ func (s *server) zipApp(ctx context.Context, name, pathInRepo, repo string, obj return buf.Bytes(), nil } +func (s *server) targzApp(ctx context.Context, name, pathInRepo, repo string, obj *object.Commit) ([]byte, error) { + fi, err := obj.Files() + if err != nil { + return nil, fmt.Errorf("listing files: %w", err) + } + buf := bytes.NewBuffer(nil) + gz := gzip.NewWriter(buf) + t := tar.NewWriter(gz) + for { + f, err := fi.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("fi.Next: %w", err) + } + prefix := "" + if pathInRepo != "" { + prefix = pathInRepo + "/" + } + if !strings.HasPrefix(f.Name, prefix) { + continue + } + if !f.Mode.IsFile() { + continue + } + p := f.Name[len(prefix):] + prefix = strings.ReplaceAll(repo, "/", "-") + outPath := path.Join(prefix, p) + err = t.WriteHeader(&tar.Header{ + Name: outPath, + Typeflag: tar.TypeReg, + Size: f.Size, + }) + if err != nil { + return nil, fmt.Errorf("Create(%q): %w", outPath, err) + } + if f.Size+int64(buf.Len()) > 10<<20 { + return nil, fmt.Errorf("archive too large") + } + rdr, err := f.Blob.Reader() + if err != nil { + return nil, fmt.Errorf("Blob.Reader: %w", err) + } + _, err = io.Copy(t, rdr) + rdr.Close() + if err != nil { + return nil, fmt.Errorf("when copying: %w", err) + } + } + if err := t.Close(); err != nil { + return nil, fmt.Errorf("when closing tar: %w", err) + } + if err := gz.Close(); err != nil { + return nil, fmt.Errorf("when closing gz: %w", err) + } + return buf.Bytes(), nil +} + func (s *server) parseAppToml(ctx context.Context, pathInRepo string, obj *object.Commit) (*appInfo, error) { p := path.Join(pathInRepo, "flow3r.toml") f, err := obj.File(p) @@ -268,6 +330,11 @@ func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appI return nil, fmt.Errorf("zipping failed: %w", err) } app.zip = zbytes + tbytes, err := s.targzApp(ctx, app.name, pathInRepo, repo, app.commitObj) + if err != nil { + return nil, fmt.Errorf("targzing failed: %w", err) + } + app.targz = tbytes return app, nil } @@ -373,15 +440,16 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) { } type app struct { - RepoURL string `json:"repoUrl"` - Commit string `json:"commit"` - DownloadURL string `json:"downloadUrl"` - Name string `json:"name"` - Menu string `json:"menu"` - Author string `json:"author"` - Description string `json:"description"` - Version int `json:"version"` - Stars int `json:"stars"` + RepoURL string `json:"repoUrl"` + Commit string `json:"commit"` + DownloadURL string `json:"downloadUrl"` + TarDownloadURL string `json:"tarDownloadUrl"` + Name string `json:"name"` + Menu string `json:"menu"` + Author string `json:"author"` + Description string `json:"description"` + Version int `json:"version"` + Stars int `json:"stars"` } type res struct { @@ -398,15 +466,16 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) { } // Guarenteed to be exactly 2 parts because of the regex resp.Apps = append(resp.Apps, app{ - RepoURL: "https://git.flow3r.garden/" + a.repository, - Commit: a.appInfo.commit, - DownloadURL: fmt.Sprintf("%sapps/zip/%s.zip", flagBaseURL, a.repository), - Name: a.appInfo.name, - Menu: a.appInfo.menu, - Author: a.appInfo.author, - Description: a.appInfo.description, - Version: a.appInfo.version, - Stars: a.appInfo.stars, + RepoURL: "https://git.flow3r.garden/" + a.repository, + Commit: a.appInfo.commit, + DownloadURL: fmt.Sprintf("%sapps/zip/%s.zip", flagBaseURL, a.repository), + TarDownloadURL: fmt.Sprintf("%sapps/tar/%s.tar.gz", flagBaseURL, a.repository), + Name: a.appInfo.name, + Menu: a.appInfo.menu, + Author: a.appInfo.author, + Description: a.appInfo.description, + Version: a.appInfo.version, + Stars: a.appInfo.stars, }) } w.Header().Add("Access-Control-Allow-Origin", "*") @@ -416,7 +485,8 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) { } var ( - reAppZipURL = regexp.MustCompile("^/api/apps/zip/([^/]+)/([^/]+).zip$") + reAppZipURL = regexp.MustCompile(`^/api/apps/zip/([^/]+)/([^/]+)\.zip$`) + reAppTargzURL = regexp.MustCompile(`^/api/apps/tar/([^/]+)/([^/]+)\.tar\.gz$`) ) func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) { @@ -445,3 +515,30 @@ func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) } + +func (s *server) handleAppTargz(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + matches := reAppTargzURL.FindStringSubmatch(r.URL.Path) + if matches == nil { + http.NotFound(w, r) + return + } + orga := matches[1] + repo := matches[2] + + apps, err := s.getApps(ctx) + if err != nil { + return + } + + for _, app := range apps { + if app.repository == fmt.Sprintf("%s/%s", orga, repo) { + w.Header().Add("Content-Type", "application/x-gzip") + w.Write(app.appInfo.targz) + return + } + } + + http.NotFound(w, r) +}