diff --git a/main.go b/main.go index 3ab761d522a6573a8d1434a6096fede430e75dc0..f6001029682b5d9eeb43060492c7410ea05110a2 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ func main() { http.HandleFunc("/api/apps.json", s.handleApps) http.HandleFunc("/api/releases.json", s.handleReleases) http.HandleFunc("/api/release/", s.handleReleaseMirror) + http.HandleFunc("/api/apps/zip/", s.handleAppZip) log.Printf("Listening on %s...", flagListen) http.ListenAndServe(flagListen, nil) } diff --git a/server_apps.go b/server_apps.go index 8d9692fbd1b6446bde113b83e7e314844cece804..d39377d5cedaa34b85ab99fca070739cd0727df8 100644 --- a/server_apps.go +++ b/server_apps.go @@ -1,6 +1,8 @@ package main import ( + "archive/zip" + "bytes" "context" "encoding/json" "fmt" @@ -35,6 +37,9 @@ type appInfo struct { version int commit string stars int + + commitObj *object.Commit + zip []byte } type GLProject struct { @@ -72,6 +77,57 @@ func (s *server) getStars(ctx context.Context, repo string) (int, error) { return project.StarCount, nil } +func (s *server) zipApp(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) + w := zip.NewWriter(buf) + 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) + fo, err := w.Create(outPath) + 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(fo, rdr) + rdr.Close() + if err != nil { + return nil, fmt.Errorf("when copying: %w", err) + } + } + if err := w.Close(); err != nil { + return nil, fmt.Errorf("when closing: %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) @@ -132,6 +188,7 @@ func (s *server) parseAppToml(ctx context.Context, pathInRepo string, obj *objec description: data.Metadata.Description, version: data.Metadata.Version, commit: obj.Hash.String(), + commitObj: obj, }, nil } @@ -204,8 +261,14 @@ func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appI if err != nil { return nil, fmt.Errorf("getting stars failed: %w", err) } - firstTime[highestVer].stars = stars - return firstTime[highestVer], nil + app := firstTime[highestVer] + app.stars = stars + zbytes, err := s.zipApp(ctx, app.name, pathInRepo, repo, app.commitObj) + if err != nil { + return nil, fmt.Errorf("zipping failed: %w", err) + } + app.zip = zbytes + return app, nil } func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) { @@ -276,7 +339,6 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) { log.Printf("App %q: couldn't parse TOML: %v", m[1], err) continue } - log.Printf("%+v", dat) n := reAppRepo.FindStringSubmatch(dat.Repo) if n == nil { log.Printf("App %q: invalid repo %q", m[1], dat.Repo) @@ -335,13 +397,10 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) { continue } // Guarenteed to be exactly 2 parts because of the regex - parts := strings.Split(a.repository, "/") - orga := parts[0] - repo := parts[1] resp.Apps = append(resp.Apps, app{ RepoURL: "https://git.flow3r.garden/" + a.repository, Commit: a.appInfo.commit, - DownloadURL: fmt.Sprintf("https://git.flow3r.garden/%s/%s/-/archive/%s/%s-%s.zip", orga, repo, a.appInfo.commit, repo, 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, @@ -355,3 +414,34 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) { j := json.NewEncoder(w) j.Encode(resp) } + +var ( + reAppZipURL = regexp.MustCompile("^/api/apps/zip/([^/]+)/([^/]+).zip$") +) + +func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + matches := reAppZipURL.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/zip") + w.Write(app.appInfo.zip) + return + } + } + + http.NotFound(w, r) +} diff --git a/server_mirror.go b/server_mirror.go index 9aff64c9e6ba97bbd285d4fe74caf5bfa0f4e675..e4f220394df1716086b7d0cf93a48be97faa64dc 100644 --- a/server_mirror.go +++ b/server_mirror.go @@ -8,6 +8,7 @@ import ( "io" "log" "net/http" + "regexp" "strings" ) @@ -98,6 +99,10 @@ func (s *server) serveMirroredFile(w http.ResponseWriter, r *http.Request, rel * http.NotFound(w, r) } +var ( + reMirrorURL = regexp.MustCompile("^/api/release/([^/]+)/([^/]+.bin)$") +) + func (s *server) handleReleaseMirror(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/server_releases.go b/server_releases.go index ffbef4ae14825ef9c76ba2e1b409a48cc6504996..7590e28769d6515bdd4bcae75bc219148da3c532 100644 --- a/server_releases.go +++ b/server_releases.go @@ -6,14 +6,9 @@ import ( "fmt" "net/http" "net/url" - "regexp" "time" ) -var ( - reMirrorURL = regexp.MustCompile("^/api/release/([^/]+)/([^/]+.bin)$") -) - type GLAssetLink struct { ID int64 `json:"id"` Name string `json:"name"`