Select Git revision
server_apps.go
Forked from
flow3r / API
40 commits behind the upstream repository.

q3k authored
server_apps.go 7.15 KiB
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"path"
"regexp"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pelletier/go-toml/v2"
)
type appDescriptor struct {
repository string
pathInRepo string
appInfo *appInfo
}
type appInfo struct {
name string
menu string
author string
description string
version int
commit string
}
var (
reAppPath = regexp.MustCompile(`^apps/([^/]+.toml)$`)
reAppRepo = regexp.MustCompile(`^([a-zA-Z\-_\.0-9]+)/([a-zA-Z\-_0-9]+)$`)
)
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)
if err != nil {
return nil, fmt.Errorf("toml open (%q) failed: %w", p, err)
}
reader, err := f.Reader()
if err != nil {
return nil, fmt.Errorf("f.Reader: %w", err)
}
defer reader.Close()
var data struct {
App struct {
Name string
Menu string
}
Entry struct {
Class string
}
Metadata struct {
Author string
License string
URL string
Description string
Version int
}
}
dec := toml.NewDecoder(reader)
err = dec.Decode(&data)
if err != nil {
return nil, fmt.Errorf("toml decode failed: %w", err)
}
if data.App.Name == "" || len(data.App.Name) > 32 {
return nil, fmt.Errorf("app name invalif")
}
sections := map[string]bool{
"Badge": true,
"Apps": true,
"Music": true,
}
if !sections[data.App.Menu] {
return nil, fmt.Errorf("app menu invalid")
}
if data.Entry.Class == "" {
return nil, fmt.Errorf("no entry class")
}
if len(data.Metadata.Author) > 32 {
return nil, fmt.Errorf("metadata author too long")
}
if len(data.Metadata.Description) > 140 {
return nil, fmt.Errorf("metadata description too long")
}
return &appInfo{
name: data.App.Name,
author: data.Metadata.Author,
menu: data.App.Menu,
description: data.Metadata.Description,
version: data.Metadata.Version,
commit: obj.Hash.String(),
}, nil
}
func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appInfo, error) {
url := "https://git.flow3r.garden/" + repo
g, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: url,
})
if err != nil {
return nil, fmt.Errorf("when cloning: %w", err)
}
cfg, err := g.Config()
if err != nil {
return nil, fmt.Errorf("when getting config: %w", err)
}
if len(cfg.Branches) < 1 {
return nil, fmt.Errorf("no branches")
}
var bname string
for name := range cfg.Branches {
bname = name
break
}
h, err := g.ResolveRevision(plumbing.Revision(bname))
if err != nil {
return nil, fmt.Errorf("resolving revision failed: %w", err)
}
err = g.Fetch(&git.FetchOptions{
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", bname, bname)),
},
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return nil, fmt.Errorf("fetch failed: %w", err)
}
obj, err := g.CommitObject(*h)
if err != nil {
return nil, fmt.Errorf("CommitObject(%v): %w", h, err)
}
highestVer := 0
highsetVerNil := true
firstTime := make(map[int]*appInfo)
for {
info, err := s.parseAppToml(ctx, pathInRepo, obj)
if err == nil {
ver := info.version
firstTime[ver] = info
if ver > highestVer || highsetVerNil {
highestVer = ver
highsetVerNil = false
}
} else {
log.Printf("%s@%s: %v", repo, obj.Hash.String(), err)
}
if len(obj.ParentHashes) == 0 {
break
}
obj, err = g.CommitObject(obj.ParentHashes[0])
if err != nil {
return nil, fmt.Errorf("CommitObject(%v): %w", h, err)
}
}
if highsetVerNil {
return nil, fmt.Errorf("no version")
}
return firstTime[highestVer], nil
}
func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
s.gitMu.Lock()
defer s.gitMu.Unlock()
if s.appRepo == nil {
git, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: "https://git.flow3r.garden/flow3r/flow3r-apps",
})
if err != nil {
return nil, err
}
s.appRepo = git
}
if time.Since(s.lastFetch) > 10*time.Second {
s.appRepo.Fetch(&git.FetchOptions{
RefSpecs: []config.RefSpec{
config.RefSpec("+refs/heads/*:refs/heads/*"),
},
Force: true,
})
}
h, err := s.appRepo.ResolveRevision(plumbing.Revision("main"))
if err != nil {
return nil, err
}
obj, err := s.appRepo.CommitObject(*h)
if err != nil {
return nil, err
}
iter, err := obj.Files()
if err != nil {
return nil, err
}
var res []*appDescriptor
for {
f, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if !f.Mode.IsFile() {
continue
}
m := reAppPath.FindStringSubmatch(f.Name)
if m == nil {
continue
}
reader, err := f.Reader()
if err != nil {
log.Printf("App %q: couldn't read TOML: %v", m[1], err)
continue
}
dec := toml.NewDecoder(reader)
var dat struct {
Repo string
PathInRepo string `toml:"path_in_repo"`
}
err = dec.Decode(&dat)
reader.Close()
if err != nil {
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)
continue
}
res = append(res, &appDescriptor{
repository: dat.Repo,
pathInRepo: dat.PathInRepo,
})
}
for _, app := range res {
info, err := s.getAppInfo(ctx, app.pathInRepo, app.repository)
if err != nil {
log.Printf("App %q: %v", app.repository, err)
continue
} else {
log.Printf("App: %q: okay", app.repository)
}
app.appInfo = info
}
log.Printf("Got %d apps", len(res))
return res, nil
}
func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apps, err := s.getApps(ctx)
if err != nil {
return
}
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"`
}
type res struct {
DT time.Time `json:"isodatetime"`
Apps []app `json:"apps"`
}
resp := res{
DT: time.Now(),
}
for _, a := range apps {
if a.appInfo == nil {
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),
Name: a.appInfo.name,
Menu: a.appInfo.menu,
Author: a.appInfo.author,
Description: a.appInfo.description,
Version: a.appInfo.version,
})
}
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Content-Type", "application/json")
j := json.NewEncoder(w)
j.Encode(resp)
}