diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..a814f42 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,46 @@ +package cache + +import ( + "time" +) + +type Cache struct { + timeout time.Duration + store map[string]cacheElement +} + +func New(timeout time.Duration) *Cache { + return &Cache{ + timeout: timeout, + store: map[string]cacheElement{}, + } +} + +func (c *Cache) Add(key string, data []byte) { + c.store[key] = cacheElement{ + cachedAt: time.Now(), + data: data, + } +} + +func (c *Cache) Get(key string) ([]byte, bool) { + elem, ok := c.store[key] + if !ok { + return nil, false + } + age := time.Since(elem.cachedAt) + if age >= c.timeout { + delete(c.store, key) + return nil, false + } + return elem.data, true +} + +func (c *Cache) Purge() { + c.store = make(map[string]cacheElement) +} + +type cacheElement struct { + data []byte + cachedAt time.Time +} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go new file mode 100644 index 0000000..aa73f92 --- /dev/null +++ b/internal/cache/cache_test.go @@ -0,0 +1,93 @@ +package cache + +import ( + "bytes" + "testing" + "time" +) + +func TestCache_Add(t *testing.T) { + // Create a new cache instance + cache := New(time.Minute) + + // Add a cache element with a key and data + key := "test" + data := []byte("test data") + cache.Add(key, data) + + // Check if the cache element exists in the store + if _, ok := cache.store[key]; !ok { + t.Errorf("Cache element with key '%s' not found", key) + } + + // Check if the data of the cache element is correct + if got := cache.store[key].data; !bytes.Equal(got, data) { + t.Errorf("Cache element data mismatch, got %v, want %v", got, data) + } +} + +func TestCache_Get(t *testing.T) { + // Create a new cache instance + cache := New(time.Minute) + + // Add a cache element with a key and data + key := "test" + data := []byte("test data") + cache.Add(key, data) + + // Retrieve the cache element using the Get method + got, ok := cache.Get(key) + + // Check if the cache element is found + if !ok { + t.Errorf("Cache element with key '%s' not found", key) + } + + // Check if the retrieved data is correct + if string(got) != string(data) { + t.Errorf("Retrieved data mismatch, got %s, want %s", got, data) + } +} + +func TestCache_Get_Expired(t *testing.T) { + // Create a new cache instance with a short timeout duration + cache := New(1 * time.Millisecond) + + // Add a cache element with a key and data + key := "test" + data := []byte("test data") + cache.Add(key, data) + + // Wait for the cache element to expire + time.Sleep(2 * time.Millisecond) + + // Retrieve the cache element using the Get method + got, ok := cache.Get(key) + + // Check if the cache element is not found + if ok { + t.Errorf("Cache element with key '%s' found, expected not found", key) + } + + // Check if the retrieved data is nil + if got != nil { + t.Errorf("Retrieved data mismatch, got %v, want nil", got) + } +} + +func TestCache_Purge(t *testing.T) { + // Create a new cache instance + cache := New(time.Minute) + + // Add multiple cache elements + cache.Add("key1", []byte("data1")) + cache.Add("key2", []byte("data2")) + + // Purge the cache + cache.Purge() + + // Check if the store is empty + if len(cache.store) != 0 { + t.Errorf("Cache store is not empty after purging") + } +} diff --git a/internal/persistence/git.go b/internal/persistence/git.go new file mode 100644 index 0000000..0a5fac1 --- /dev/null +++ b/internal/persistence/git.go @@ -0,0 +1,92 @@ +package persistence + +import ( + "fmt" + "os" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + ssh2 "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/go-git/go-git/v5/storage/memory" + "golang.org/x/crypto/ssh" +) + +type gitrepo struct { + repo *git.Repository + fs billy.Filesystem +} + +func NewGitRepo(keypath, pass, url string) (Persistence, error) { + gr := &gitrepo{} + pem, err := os.ReadFile(keypath) + if err != nil { + return gr, err + } + signer, err := ssh.ParsePrivateKeyWithPassphrase(pem, []byte(pass)) + if err != nil { + return gr, err + } + auth := &ssh2.PublicKeys{User: "git", Signer: signer} + gr.fs = memfs.New() + gr.repo, err = git.Clone(memory.NewStorage(), gr.fs, &git.CloneOptions{ + URL: url, + Auth: auth, + Progress: os.Stdout, + Mirror: true, + }) + return gr, err +} + +func (gr *gitrepo) Filesystem() billy.Filesystem { + return gr.fs +} + +func (gr *gitrepo) Branches() ([]string, error) { + var branches []string + iter, err := gr.repo.Branches() + if err != nil { + return branches, err + } + _ = iter.ForEach(func(this *plumbing.Reference) error { + branches = append(branches, this.Name().String()) + return nil + }) + return branches, nil +} + +func (gr *gitrepo) branch(name string) *plumbing.Reference { + var ref *plumbing.Reference + iter, err := gr.repo.Branches() + if err != nil { + return ref + } + _ = iter.ForEach(func(this *plumbing.Reference) error { + if this.Name().String() == name { + ref = this + } + return nil + }) + return ref +} + +func (gr *gitrepo) CheckoutBranch(name string) error { + worktree, err := gr.repo.Worktree() + if err != nil { + return err + } + branch := gr.branch(name) + if branch == nil { + return fmt.Errorf("Branch '%s' does not exist", name) + } + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: branch.Name(), + Force: true, + }) + if err != nil { + return err + } + gr.fs = worktree.Filesystem + return nil +} diff --git a/internal/persistence/local.go b/internal/persistence/local.go new file mode 100644 index 0000000..28ad322 --- /dev/null +++ b/internal/persistence/local.go @@ -0,0 +1,29 @@ +package persistence + +import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/osfs" +) + +type local struct { + fs billy.Filesystem +} + +func NewLocal(path string) (Persistence, error) { + l := &local{ + fs: osfs.New(path), + } + return l, nil +} + +func (l *local) Filesystem() billy.Filesystem { + return l.fs +} + +func (l *local) Branches() ([]string, error) { + return []string{"local"}, nil +} + +func (l *local) CheckoutBranch(name string) error { + return nil +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go new file mode 100644 index 0000000..714f466 --- /dev/null +++ b/internal/persistence/persistence.go @@ -0,0 +1,31 @@ +package persistence + +import ( + "fmt" + + "github.com/go-git/go-billy/v5" +) + +type Persistence interface { + Filesystem() billy.Filesystem + Branches() ([]string, error) + CheckoutBranch(string) error +} + +type Config struct { + Filepath string + Git struct { + Keyfile string + Pass string + URL string + } +} + +func New(c Config) (Persistence, error) { + if c.Git.URL != "" { + return NewGitRepo(c.Git.Keyfile, c.Git.Pass, c.Git.URL) + } else if c.Filepath != "" { + return NewLocal(c.Filepath) + } + return nil, fmt.Errorf("Neither a git url nor a local file path was provided") +}