From a847a989c09b1b4740cea2d295e5bfbeef309b4e Mon Sep 17 00:00:00 2001 From: MRtecno98 Date: Wed, 31 Jul 2024 23:35:14 +0200 Subject: [PATCH] implement database cache --- bucket/cache.go | 89 ++++++++++++++++++++ bucket/config.go | 24 +++++- bucket/context.go | 86 ++++++++++++++++--- bucket/database.go | 196 +++++++++++++++++++++++++++++++++++++++++++ bucket/map.go | 97 +++++++++++++++++++++ bucket/platform.go | 11 +++ bucket/repository.go | 5 ++ bucket/util.go | 27 ++++++ go.mod | 9 +- go.sum | 14 +++- 10 files changed, 535 insertions(+), 23 deletions(-) create mode 100644 bucket/cache.go create mode 100644 bucket/database.go create mode 100644 bucket/map.go diff --git a/bucket/cache.go b/bucket/cache.go new file mode 100644 index 0000000..9e3a3e6 --- /dev/null +++ b/bucket/cache.go @@ -0,0 +1,89 @@ +package bucket + +func (cp *CachedPlugin) GetName() string { + if cp.RemotePlugin != nil { + return cp.RemotePlugin.GetName() + } + + return cp.name +} + +func (cp *CachedPlugin) GetIdentifier() string { + if cp.RemotePlugin != nil { + return cp.RemotePlugin.GetIdentifier() + } + + return cp.RemoteIdentifier +} + +func (cp *CachedPlugin) GetAuthors() []string { + if cp.RemotePlugin != nil { + return cp.RemotePlugin.GetAuthors() + } + + return cp.authors +} + +func (cp *CachedPlugin) GetDescription() string { + if cp.RemotePlugin != nil { + return cp.RemotePlugin.GetDescription() + } + + return cp.description +} + +func (cp *CachedPlugin) GetWebsite() string { + if cp.RemotePlugin != nil { + return cp.RemotePlugin.GetWebsite() + } + + return cp.website +} + +func (cp *CachedPlugin) requestIfMissing() error { + if cp.RemotePlugin == nil { + return cp.Request() + } else { + return nil + } +} + +func (cp *CachedPlugin) GetLatestVersion() (RemoteVersion, error) { + if err := cp.requestIfMissing(); err != nil { + return nil, err + } + + return cp.RemotePlugin.GetLatestVersion() +} + +func (cp *CachedPlugin) GetVersions() ([]RemoteVersion, error) { + if err := cp.requestIfMissing(); err != nil { + return nil, err + } + + return cp.RemotePlugin.GetVersions() +} + +func (cp *CachedPlugin) GetVersion(version string) (RemoteVersion, error) { + if err := cp.requestIfMissing(); err != nil { + return nil, err + } + + return cp.RemotePlugin.GetVersion(version) +} + +func (cp *CachedPlugin) GetVersionIdentifiers() ([]string, error) { + if err := cp.requestIfMissing(); err != nil { + return nil, err + } + + return cp.RemotePlugin.GetVersionIdentifiers() +} + +func (cp *CachedPlugin) GetLatestCompatible(plt PlatformType) (RemoteVersion, error) { + if err := cp.requestIfMissing(); err != nil { + return nil, err + } + + return cp.RemotePlugin.GetLatestCompatible(plt) +} diff --git a/bucket/config.go b/bucket/config.go index 7dd87f5..fb9d10e 100644 --- a/bucket/config.go +++ b/bucket/config.go @@ -1,6 +1,8 @@ package bucket import ( + "context" + "fmt" "io" "log" "os" @@ -24,8 +26,26 @@ type Config struct { } type RepositoryConfig struct { - Name string `yaml:"name"` - Options map[string]string `yaml:"options"` + Name string `yaml:"name"` + Provider string `yaml:"provider"` + Options map[string]string `yaml:"options"` +} + +func (rc *RepositoryConfig) GetName() string { + if rc.Name == "" { + return rc.Provider + } else { + return rc.Name + } +} + +func (rc *RepositoryConfig) MakeRepository(oc *OpenContext) (*NamedRepository, error) { + if constr, ok := Repositories[rc.Provider]; ok { + return &NamedRepository{RepositoryConfig: *rc, + Repository: constr(context.TODO(), oc, rc.Options)}, nil + } else { + return nil, fmt.Errorf("unknown repository: %s", rc.Name) + } } func (c *Config) MakeWorkspace() (*Workspace, error) { diff --git a/bucket/context.go b/bucket/context.go index ddbc213..f3810d2 100644 --- a/bucket/context.go +++ b/bucket/context.go @@ -1,7 +1,7 @@ package bucket import ( - "context" + "database/sql" "fmt" "log" "os" @@ -15,6 +15,8 @@ import ( const SIMILARITY_THRESHOLD float64 = 0.51 +var DEFAULT_REPOSITORIES = [...]string{"spigotmc", "modrinth"} + type Context struct { Name string `yaml:"name"` URL string `yaml:"url"` @@ -25,7 +27,11 @@ type OpenContext struct { Fs afero.Afero LocalConfig *Config Platform Platform - Repositories []Repository + Repositories map[string]NamedRepository + + Plugins *SymmetricBiMap[string, CachedPlugin] + + Database *sql.DB } type Workspace struct { @@ -34,6 +40,7 @@ type Workspace struct { func (w *Workspace) RunWithContext(name string, action func(*OpenContext, *log.Logger) error) error { var res error = &multierror.Error{Errors: []error{}} + var newline bool = false for _, c := range w.Contexts { fmt.Printf(":%s [%s]\n", name, c.Name) @@ -51,14 +58,17 @@ func (w *Workspace) RunWithContext(name string, action func(*OpenContext, *log.L logger.Println() } } + + newline = true } if err != nil { logger.Printf(":%s [%s] FAILED: %s\n\n", name, c.Name, err) + newline = true } } - if len(w.Contexts) > 1 { + if !newline && len(w.Contexts) > 1 { fmt.Println() } @@ -71,7 +81,7 @@ func (w *Workspace) RunWithContext(name string, action func(*OpenContext, *log.L func (w *Workspace) CloseWorkspace() { w.RunWithContext("close", func(c *OpenContext, log *log.Logger) error { - c.Fs.Close() + c.CloseContext() return nil }) } @@ -88,13 +98,24 @@ func (c Context) OpenContext() (*OpenContext, error) { conf.Collapse(GlobalConfig) // Also add base options } - ctx := &OpenContext{Context: c, Fs: afero.Afero{Fs: fs}, LocalConfig: conf} + ctx := &OpenContext{Context: c, Fs: afero.Afero{Fs: fs}, + LocalConfig: conf, + Repositories: make(map[string]NamedRepository), + Plugins: NewPluginBiMap()} - if err := ctx.LoadRepositories(); err != nil { - return nil, err + return ctx, Parallelize( + ctx.LoadRepositories, + ctx.LoadPlatform, + ctx.InitialiazeDatabase) +} + +func (c *OpenContext) CloseContext() { + if c.Database != nil { + // c.SavePluginDatabase() // Maybe not necessary + c.Database.Close() } - return ctx, ctx.LoadPlatform() + c.Fs.Close() } func (c *OpenContext) PlatformName() string { @@ -135,6 +156,10 @@ func (c *OpenContext) LoadPlatform() error { func (c *OpenContext) ResolvePlugin(plugin Plugin) (RemotePlugin, error) { var gerr error + if rem, ok := c.Plugins.GetAny(plugin.GetIdentifier()); ok { + return &rem, nil + } + for _, r := range c.Repositories { if _, candidates, err := r.Resolve(plugin); err != nil { gerr = multierror.Append(gerr, err) @@ -171,19 +196,54 @@ func (c *OpenContext) ResolvePlugin(plugin Plugin) (RemotePlugin, error) { continue } - return scores[match], nil + res := CachedMatch(plugin, scores[match], r, match) + if err := c.SavePlugin(res); err != nil { + return nil, err + } + + return &res, nil } } return nil, gerr } +func (c *OpenContext) RepositoryByNameOrProvider(name string) *NamedRepository { + if v, ok := c.Repositories[name]; ok { + return &v + } + + if v := c.RepositoryByProvider(name); v != nil { + return v + } + + return nil +} + +func (c *OpenContext) RepositoryByProvider(provider string) *NamedRepository { + for _, v := range c.Repositories { + if v.Repository.Provider() == provider { + return &v + } + } + + return nil +} + func (c *OpenContext) LoadRepositories() error { - for _, v := range c.Config().Repositories { - if constr, ok := Repositories[v.Name]; ok { - c.Repositories = append(c.Repositories, constr(context.Background(), c, v.Options)) // TODO: Use another context + repos := c.Config().Repositories + if len(repos) == 0 { + repos = make([]RepositoryConfig, len(DEFAULT_REPOSITORIES)) + for i, v := range DEFAULT_REPOSITORIES { + repos[i] = RepositoryConfig{Provider: v} + } + } + + for _, v := range repos { + if r, err := v.MakeRepository(c); err != nil { + return err } else { - return fmt.Errorf("unknown repository: %s", v.Name) + c.Repositories[v.GetName()] = *r } } diff --git a/bucket/database.go b/bucket/database.go new file mode 100644 index 0000000..4b6e55c --- /dev/null +++ b/bucket/database.go @@ -0,0 +1,196 @@ +package bucket + +import ( + "database/sql" + "fmt" + "log" + "strings" + + "github.com/MRtecno98/afero" + "github.com/MRtecno98/afero/sqlitevfs" +) + +const DATABASE_NAME = "bucket.db" + +type CachedPlugin struct { + RemotePlugin + + Repository NamedRepository + + name string + LocalIdentifier string + RemoteIdentifier string + + authors []string + description string + website string + + Confidence float64 +} + +func CachedMatch(local Plugin, remote RemotePlugin, repo NamedRepository, conf float64) CachedPlugin { + return CachedPlugin{ + RemotePlugin: remote, + LocalIdentifier: local.GetIdentifier(), + RemoteIdentifier: remote.GetIdentifier(), + Repository: repo, + Confidence: conf} +} + +func NewPluginBiMap() *SymmetricBiMap[string, CachedPlugin] { + return NewSymmetricBiMap(func(el CachedPlugin) (string, string) { + return el.LocalIdentifier, el.RemoteIdentifier + }) +} + +func (cp *CachedPlugin) Request() error { + if cp.RemotePlugin != nil { + return nil + } + + remote, err := cp.Repository.Get(cp.RemoteIdentifier) + if err != nil { + return err + } + + cp.RemotePlugin = remote + + return nil +} + +func (c *OpenContext) InitialiazeDatabase() error { + if DEBUG { + log.Printf("initializing database for %s\n", c.Name) + } + + if _, ok := c.Fs.Fs.(*afero.MemMapFs); ok { + // TODO: Fix database for in-memory filesystem + log.Printf("%s: in-memory filesystem not supported for database\n", c.Name) + return nil + } + + sqlitevfs.RegisterVFS(c.Name, c.Fs) + + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?vfs=%s", DATABASE_NAME, c.Name)) + if err != nil { + return err + } + + c.Database = db + + if err = c.CreateTables(); err != nil { + db.Close() + return fmt.Errorf("sql: %w", err) + } + + return nil +} + +func (c *OpenContext) LoadPluginDatabase() error { + rows, err := c.Database.Query(`SELECT * FROM plugins`) + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var plugin CachedPlugin + var authors string + var repo string + + if err := rows.Scan(&plugin.LocalIdentifier, + &plugin.RemoteIdentifier, &plugin.name, + &repo, &plugin.Confidence, &authors, + &plugin.description, &plugin.website); err != nil { + return err + } + + plugin.authors = strings.Split(authors, ",") + repository := c.RepositoryByNameOrProvider(repo) + if repository == nil { + log.Printf("warn: repository %s not found for plugin record %s\n", repo, plugin.LocalIdentifier) + continue + } + + plugin.Repository = *repository + + c.Plugins.Put(plugin) + } + + return nil +} + +func (c *OpenContext) SavePlugin(plugin CachedPlugin) error { + tx, err := c.Database.Begin() + if err != nil { + return err + } + + if c._savePlugin(plugin) != nil { + return tx.Rollback() + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("plugin save: %w", err) + } + + c.Plugins.Put(plugin) + return nil +} + +func (c *OpenContext) _savePlugin(plugin CachedPlugin) error { + if _, err := c.Database.Exec( + `INSERT INTO plugins + (identifier, remote_identifier, + name, repository, confidence, + authors, description, website) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + plugin.LocalIdentifier, plugin.RemoteIdentifier, + plugin.GetName(), plugin.Repository.GetName(), plugin.Confidence, + strings.Join(plugin.GetAuthors(), ","), + plugin.GetDescription(), plugin.GetWebsite()); err != nil { + return fmt.Errorf("db save (no data was modified): %w", err) + } + + return nil +} + +func (c *OpenContext) SavePluginDatabase() error { + tx, err := c.Database.Begin() + if err != nil { + return err + } + + for _, plugin := range c.Plugins.Values() { + if c._savePlugin(plugin) != nil { + return tx.Rollback() + } + } + + return tx.Commit() +} + +func (c *OpenContext) CreateTables() error { + if _, err := c.Database.Exec(` + CREATE TABLE IF NOT EXISTS plugins ( + identifier VARCHAR(255) PRIMARY KEY, + remote_identifier VARCHAR(255), + name VARCHAR(255), + repository VARCHAR(255), + confidence REAL, + authors TEXT, + description TEXT, + website TEXT + );`); err != nil { + return err + } + + if _, err := c.Database.Exec(` + CREATE INDEX IF NOT EXISTS plugins_remote_id ON plugins (remote_identifier); + `); err != nil { + return err + } + + return nil +} diff --git a/bucket/map.go b/bucket/map.go new file mode 100644 index 0000000..3bc0d24 --- /dev/null +++ b/bucket/map.go @@ -0,0 +1,97 @@ +package bucket + +import "cmp" + +type BiMap[K1 cmp.Ordered, K2 cmp.Ordered, V any] struct { + first map[K1]V + second map[K2]V + + keyfunc func(el V) (first K1, second K2) +} + +func NewBiMap[K1 cmp.Ordered, K2 cmp.Ordered, V any]( + keyfunc func(el V) (first K1, second K2)) *BiMap[K1, K2, V] { + return &BiMap[K1, K2, V]{ + first: make(map[K1]V), + second: make(map[K2]V), + keyfunc: keyfunc, + } +} + +func (bm *BiMap[K1, K2, V]) Put(value V) { + first, second := bm.keyfunc(value) + bm.first[first] = value + bm.second[second] = value +} + +func (bm *BiMap[K1, K2, V]) GetFirst(key K1) (V, bool) { + value, ok := bm.first[key] + return value, ok +} + +func (bm *BiMap[K1, K2, V]) GetSecond(key K2) (V, bool) { + value, ok := bm.second[key] + return value, ok +} + +func (bm *BiMap[K1, K2, V]) DeleteFirst(key K1) { + value, ok := bm.first[key] + if ok { + delete(bm.first, key) + _, sk := bm.keyfunc(value) + delete(bm.second, sk) + } +} + +func (bm *BiMap[K1, K2, V]) DeleteSecond(key K2) { + value, ok := bm.second[key] + if ok { + delete(bm.second, key) + fk, _ := bm.keyfunc(value) + delete(bm.first, fk) + } +} + +func (bm *BiMap[K1, K2, V]) Values() []V { + var values []V + for _, v := range bm.first { + values = append(values, v) + } + return values +} + +type SymmetricBiMap[K cmp.Ordered, V any] struct { + BiMap[K, K, V] +} + +func NewSymmetricBiMap[K cmp.Ordered, V any]( + keyfunc func(el V) (first K, second K)) *SymmetricBiMap[K, V] { + return &SymmetricBiMap[K, V]{ + BiMap: BiMap[K, K, V]{ + first: make(map[K]V), + second: make(map[K]V), + keyfunc: keyfunc, + }, + } +} + +func (sbm *SymmetricBiMap[K, V]) GetAny(key K) (V, bool) { + value, ok := sbm.GetFirst(key) + if ok { + return value, ok + } + + return sbm.GetSecond(key) +} + +func (sbm *SymmetricBiMap[K, V]) Delete(key K) { + sbm.DeleteFirst(key) + sbm.DeleteSecond(key) +} + +func (sbm *SymmetricBiMap[K, V]) GetStrict(key K) (V, bool) { + v, a := sbm.GetFirst(key) + _, b := sbm.GetSecond(key) + + return v, a && b +} diff --git a/bucket/platform.go b/bucket/platform.go index e1db7c6..7221050 100644 --- a/bucket/platform.go +++ b/bucket/platform.go @@ -68,6 +68,17 @@ func BufferedDecode(decode func(in []byte, out any) error) Decoder { } } +func CompatiblePlatforms(p PlatformCompatible) []PlatformType { + var out []PlatformType + for _, v := range platforms { + if p.Compatible(v.Platform) { + out = append(out, v.Platform) + } + } + + return out +} + func (p *PluginCachePlatform) Plugins() ([]Plugin, []error, error) { if p.PluginsCache != nil { return p.PluginsCache, nil, nil diff --git a/bucket/repository.go b/bucket/repository.go index 4941fb1..67c9839 100644 --- a/bucket/repository.go +++ b/bucket/repository.go @@ -64,6 +64,11 @@ type HttpRepository struct { HttpClient *resty.Client } +type NamedRepository struct { + Repository + RepositoryConfig +} + func NewHttpRepository(endpoint string) *HttpRepository { return &HttpRepository{ Endpoint: endpoint, diff --git a/bucket/util.go b/bucket/util.go index 4ff9497..747af3a 100644 --- a/bucket/util.go +++ b/bucket/util.go @@ -4,6 +4,7 @@ import ( "archive/zip" "cmp" "slices" + "sync" "github.com/MRtecno98/afero" "github.com/MRtecno98/afero/zipfs" @@ -40,3 +41,29 @@ func Distinct[T cmp.Ordered](slice []T) []T { slices.Sort(slice) return slices.Compact(slice) } + +func Parallelize(tasks ...func() error) error { + var wait sync.WaitGroup + wait.Add(len(tasks)) + + errs := make(chan error, len(tasks)) + + for _, task := range tasks { + go func(task func() error) { + defer wait.Done() + if err := task(); err != nil { + errs <- err + } + }(task) + } + + wait.Wait() + + select { + case err := <-errs: + return err + default: + } + + return nil +} diff --git a/go.mod b/go.mod index 3cdadc4..1f68d23 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,23 @@ go 1.21 toolchain go1.21.4 require ( - github.com/MRtecno98/afero v1.9.3 + github.com/MRtecno98/afero v1.9.6 github.com/gnames/levenshtein v0.4.0 github.com/go-resty/resty/v2 v2.13.1 github.com/hashicorp/go-multierror v1.1.1 + github.com/mattn/go-sqlite3 v1.14.22 github.com/sunxyw/go-spiget v1.0.0 github.com/urfave/cli/v2 v2.27.2 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 gopkg.in/yaml.v2 v2.4.0 ) -require golang.org/x/text v0.16.0 // indirect - require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/gnames/gnfmt v0.4.3 // indirect github.com/gnames/gnuuid v0.1.1 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/fs v0.1.0 // indirect @@ -30,10 +29,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/sftp v1.13.6 // indirect + github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index f56d6c7..52565c7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/MRtecno98/afero v1.9.3 h1:Vt46x9cBTciIT+xGgr8U0i9VZfflmWHrbBuMbvcMW4g= -github.com/MRtecno98/afero v1.9.3/go.mod h1:uD70E3Djtsat89CLs8PzhhPmTedzWLmdTYIU8tYV8FM= +github.com/MRtecno98/afero v1.9.6 h1:RsR8Pj8x9j8BwUWa1NxZEoB+zTioya/LuqTOitmxDPQ= +github.com/MRtecno98/afero v1.9.6/go.mod h1:BD/RuRl2aY6snOSQhMyCEgV17APxCDRQMksl4gDR5Uc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -15,14 +15,15 @@ github.com/gnames/levenshtein v0.4.0/go.mod h1:SxfO0pn22XHRXFSuREr8SWwpzc/OKy/Ar github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -41,6 +42,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= @@ -50,6 +54,8 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361 h1:vAKifIJuYY306ZJSrwDgKonWcJGELijdaenABqbV03E= +github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=