GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package plugin
  6
  7import (
  8	"encoding/json"
  9	"fmt"
 10	"path/filepath"
 11	"reflect"
 12	"slices"
 13	"strings"
 14
 15	"github.com/goccy/go-yaml"
 16	"github.com/hashicorp/go-getter"
 17	"github.com/samber/oops"
 18	"github.com/tetratelabs/wazero"
 19	"gitroot.dev/libs/golang/glob"
 20	pluginLib "gitroot.dev/libs/golang/plugin/model"
 21	"gitroot.dev/server/logger"
 22	"gitroot.dev/server/user"
 23)
 24
 25type Plugin struct {
 26	Url            string `yaml:",omitempty"`
 27	Checksum       string `yaml:",omitempty"`
 28	Name           string
 29	Version        string
 30	Active         bool
 31	compiledModule wazero.CompiledModule
 32	commiter       *user.Commiter
 33	Run            []PluginRun
 34}
 35
 36type PluginRun struct {
 37	pluginLib.PluginRun `yaml:",inline"`
 38	glob                *glob.Glob
 39	write               PluginWrite
 40}
 41
 42type PluginWrite struct { //TODO not implemented
 43	git []PluginWriteRight
 44	web []PluginWriteRight
 45}
 46
 47type PluginWriteRight struct {
 48	pluginLib.PluginWriteRight
 49	glob *glob.Glob
 50}
 51
 52func pluginLibToPlugin(run []pluginLib.PluginRun) ([]PluginRun, error) {
 53	p := make([]PluginRun, len(run))
 54	for i, pl := range run {
 55		g, err := glob.NewGlob(pl.Path)
 56		if err != nil {
 57			return nil, oops.Wrapf(err, "can't glob %s", pl.Path)
 58		}
 59		pluginWrite := PluginWrite{
 60			git: make([]PluginWriteRight, len(pl.Write.Git)),
 61			web: make([]PluginWriteRight, len(pl.Write.Web)),
 62		}
 63		for j, pw := range pl.Write.Git {
 64			g, err := glob.NewGlob(pl.Path)
 65			if err != nil {
 66				return nil, oops.Wrapf(err, "can't glob %s", pl.Path)
 67			}
 68			pluginWrite.git[j] = PluginWriteRight{PluginWriteRight: pw, glob: g}
 69		}
 70		for j, pw := range pl.Write.Web {
 71			g, err := glob.NewGlob(pl.Path)
 72			if err != nil {
 73				return nil, oops.Wrapf(err, "can't glob %s", pl.Path)
 74			}
 75			pluginWrite.web[j] = PluginWriteRight{PluginWriteRight: pw, glob: g}
 76		}
 77		p[i] = PluginRun{PluginRun: pl, glob: g, write: pluginWrite}
 78	}
 79	return p, nil
 80}
 81
 82func ParsePlugins(fileContent []byte, withRun bool) ([]Plugin, error) {
 83	pluginsRaw := make([]Plugin, 0)
 84	if err := yaml.Unmarshal(fileContent, &pluginsRaw); err != nil {
 85		return nil, oops.Wrapf(err, "can't parse yaml config")
 86	}
 87
 88	plugins := make([]Plugin, len(pluginsRaw))
 89	for i, plugin := range pluginsRaw {
 90		plugins[i] = plugin.DetermineNameAndVersionFromUrl()
 91	}
 92	if withRun {
 93		for i, plugin := range plugins {
 94			for j, pluginRun := range plugin.Run {
 95				g, err := glob.NewGlob(pluginRun.Path)
 96				if err != nil {
 97					return nil, oops.Wrapf(err, "can't glob %s", pluginRun.Path)
 98				}
 99				plugins[i].Run[j].glob = g
100				for h, pluginWrite := range pluginRun.Write.Git {
101					g, err = glob.NewGlob(pluginWrite.Path)
102					if err != nil {
103						return nil, oops.Wrapf(err, "can't glob git %s", pluginRun.Path)
104					}
105					if plugins[i].Run[j].write.git == nil {
106						plugins[i].Run[j].write.git = make([]PluginWriteRight, len(pluginRun.Write.Git))
107					}
108					plugins[i].Run[j].write.git[h] = PluginWriteRight{PluginWriteRight: pluginWrite, glob: g}
109				}
110				for h, pluginWrite := range pluginRun.Write.Web {
111					g, err = glob.NewGlob(pluginWrite.Path)
112					if err != nil {
113						return nil, oops.Wrapf(err, "can't glob git %s", pluginRun.Path)
114					}
115					if plugins[i].Run[j].write.web == nil {
116						plugins[i].Run[j].write.web = make([]PluginWriteRight, len(pluginRun.Write.Web))
117					}
118					plugins[i].Run[j].write.web[h] = PluginWriteRight{PluginWriteRight: pluginWrite, glob: g}
119				}
120			}
121		}
122	}
123	return plugins, nil
124}
125
126func (p Plugin) SetActive(active bool) Plugin {
127	return Plugin{
128		Url:            p.Url,
129		Checksum:       p.Checksum,
130		Name:           p.Name,
131		Version:        p.Version,
132		Active:         active,
133		compiledModule: p.compiledModule,
134		commiter:       p.commiter,
135		Run:            p.Run,
136	}
137}
138
139func (p Plugin) SetRun(run []PluginRun) Plugin {
140	return Plugin{
141		Url:            p.Url,
142		Checksum:       p.Checksum,
143		Name:           p.Name,
144		Version:        p.Version,
145		Active:         p.Active,
146		compiledModule: p.compiledModule,
147		commiter:       p.commiter,
148		Run:            run,
149	}
150}
151
152func (p Plugin) UpdateVersion(version string) Plugin {
153	return Plugin{
154		Url:            p.Url,
155		Checksum:       p.Checksum,
156		Name:           p.Name,
157		Version:        version,
158		Active:         p.Active,
159		compiledModule: p.compiledModule,
160		commiter:       p.commiter,
161		Run:            p.Run,
162	}
163}
164
165func (p Plugin) OverrideWith(other Plugin) Plugin {
166	copy := Plugin{
167		Url:            p.Url,
168		Checksum:       p.Checksum,
169		Name:           p.Name,
170		Version:        p.Version,
171		Active:         p.Active,
172		compiledModule: p.compiledModule,
173		commiter:       p.commiter,
174		Run:            p.Run,
175	}
176	if copy.Url == "" {
177		copy.Url = other.Url
178	}
179	if copy.Checksum == "" {
180		copy.Checksum = other.Checksum
181	}
182	if copy.Name == "" {
183		copy.Name = other.Name
184	}
185	if copy.Version == "" {
186		copy.Version = other.Version
187	}
188	if !copy.Active {
189		copy.Active = other.Active
190	}
191	if len(copy.Run) == 0 {
192		copy.Run = other.Run
193	}
194	if copy.compiledModule == nil {
195		copy.compiledModule = other.compiledModule
196	}
197	if copy.commiter == nil || copy.commiter.Signer == nil {
198		copy.commiter = other.commiter
199	}
200	return copy
201}
202
203func (p Plugin) Commiter() *user.Commiter {
204	return p.commiter
205}
206
207func (p Plugin) DetermineNameAndVersionFromUrl() Plugin {
208	if p.Url != "" { //if child repo no url
209		_, versions := filepath.Split(p.Url)
210		nameVersion, _, _ := strings.Cut(versions, ".wasm")
211		name, version, found := strings.Cut(nameVersion, "-")
212		if !found && p.Name == "" {
213			name = p.Url
214			version = "unknown"
215		}
216		return p.OverrideWith(Plugin{Name: name}).UpdateVersion(version)
217	}
218	return p
219}
220
221func (p Plugin) uuid() string {
222	return fmt.Sprintf("%s-%s", p.Name, p.Version)
223}
224
225func (pr PluginRun) Marshal() ([]byte, error) {
226	return json.Marshal(pr.Configuration)
227}
228
229func (m *Manager) PathWasm(p Plugin) string {
230	return filepath.Join(m.conf.GetDirPathDataPlugin(p.Name), fmt.Sprintf("%s-%s.wasm", p.Name, p.Version))
231}
232
233func (m *Manager) Install(p Plugin) error {
234	url := p.Url
235	if p.Checksum != "" {
236		url = fmt.Sprintf("%s?checksum=%s", url, p.Checksum)
237	}
238	path := m.PathWasm(p)
239	m.logger.Debug("install plugin", logger.NewLoggerPair("url", url), logger.NewLoggerPair("path", path))
240	httpGetter := &getter.HttpGetter{
241		Netrc: true,
242	}
243	err := getter.GetFile(path, url, getter.WithGetters(map[string]getter.Getter{
244		"file":  &getter.FileGetter{Copy: true},
245		"http":  httpGetter,
246		"https": httpGetter,
247	}))
248	return err
249}
250
251func (m *Manager) Equal(aPlugin, bPlugin Plugin) bool {
252	return aPlugin.Name == bPlugin.Name &&
253		aPlugin.Active == bPlugin.Active &&
254		m.equalPluginRun(aPlugin.Run, bPlugin.Run)
255}
256
257func (m *Manager) equalPluginRun(aPlugin, bPlugin []PluginRun) bool {
258	if len(aPlugin) == len(bPlugin) {
259		for i, w := range aPlugin {
260			isEqual := w.Path == bPlugin[i].Path &&
261				slices.Equal(w.Branch, bPlugin[i].Branch) &&
262				slices.Equal(w.When, bPlugin[i].When) &&
263				m.equalPluginWrite(w.Write, bPlugin[i].Write) &&
264				reflect.DeepEqual(w.Configuration, bPlugin[i].Configuration)
265			if !isEqual {
266				return false
267			}
268		}
269		return true
270	}
271	return false
272}
273
274func (m *Manager) equalPluginWrite(aPlugin, bPlugin pluginLib.PluginWrite) bool {
275	if len(aPlugin.Git) == len(bPlugin.Git) {
276		for i, w := range aPlugin.Git {
277			if w.Path != bPlugin.Git[i].Path || slices.Equal(w.Can, bPlugin.Git[i].Can) {
278				return false
279			}
280		}
281		return true
282	}
283	return false
284}