Skip to content
Snippets Groups Projects
Select Git revision
  • 288f4323727f1505a57f4648aa3d4de2f2e91f49
  • main default protected
2 results

server_apps.go

Blame
  • Forked from flow3r / API
    40 commits behind the upstream repository.
    Serge Bazanski's avatar
    q3k authored
    288f4323
    History
    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)
    }